@adecore/agents 0.39.0 → 0.40.1
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/async.d.ts +5 -0
- package/dist/async.js +13 -5
- package/dist/chat/acp-protocol.js +3 -5
- package/dist/chat/chat-core.js +0 -2
- package/dist/chat/chat-process.d.ts +1 -0
- package/dist/chat/chat-process.js +1 -1
- package/dist/chat/chat-session.js +6 -8
- package/dist/chat/claude-protocol.js +1 -6
- package/dist/chat/claude-transcript.js +1 -3
- package/dist/chat/codex-protocol.js +1 -6
- package/dist/chat/projector.js +1 -3
- package/dist/chat/request-summary.js +1 -3
- package/dist/coded-error.js +1 -5
- package/dist/context/argv.js +1 -2
- package/dist/context/verb.js +1 -3
- package/dist/error-text.d.ts +1 -0
- package/dist/error-text.js +4 -0
- package/dist/fields.d.ts +3 -0
- package/dist/fields.js +12 -0
- package/dist/fs.d.ts +1 -0
- package/dist/fs.js +8 -4
- package/dist/lineage.js +11 -5
- package/dist/messages/notice-store.js +3 -4
- package/dist/providers/accounts/acp-account.js +3 -20
- package/dist/providers/accounts/service.d.ts +1 -2
- package/dist/providers/accounts/service.js +5 -9
- package/dist/providers/accounts/status.js +1 -3
- package/dist/providers/acp-agent-state.js +1 -3
- package/dist/providers/claude.js +0 -1
- package/dist/providers/codex-models.json +3 -2
- package/dist/providers/codex.js +0 -1
- package/dist/providers/detect.js +1 -1
- package/dist/providers/shell-commands.js +1 -2
- package/dist/run-process.js +1 -3
- package/dist/skills.d.ts +1 -1
- package/dist/skills.js +5 -8
- package/dist/tasks/background-limit.js +3 -2
- package/dist/tasks/task-coordinator.js +11 -17
- package/dist/testing/watch.js +0 -1
- package/dist/usage/limits/monitor.js +1 -2
- package/dist/usage/prices-snapshot.json +8 -0
- package/dist/usage/readers/claude.js +3 -10
- package/dist/usage/readers/codex.js +2 -9
- package/dist/usage/readers/grok.js +3 -10
- package/dist/usage/readers/host-log.js +3 -10
- package/dist/usage/record.d.ts +2 -0
- package/dist/usage/record.js +13 -0
- package/package.json +6 -6
- package/src/async.ts +23 -5
- package/src/chat/acp-backend.ts +121 -0
- package/src/chat/acp-protocol.ts +172 -6
- package/src/chat/attachment-store.ts +21 -0
- package/src/chat/backend.ts +0 -2
- package/src/chat/bookmark-store.ts +23 -0
- package/src/chat/chat-core.ts +208 -7
- package/src/chat/chat-log.ts +19 -0
- package/src/chat/chat-process.ts +52 -1
- package/src/chat/chat-session.ts +401 -10
- package/src/chat/chat-store.ts +50 -0
- package/src/chat/chat-title.ts +9 -0
- package/src/chat/claude-backend.ts +50 -0
- package/src/chat/claude-protocol.ts +155 -8
- package/src/chat/claude-title.ts +10 -0
- package/src/chat/claude-transcript.ts +47 -4
- package/src/chat/codex-backend.ts +131 -0
- package/src/chat/codex-protocol.ts +180 -8
- package/src/chat/codex-thread.ts +23 -0
- package/src/chat/composer-preferences.ts +6 -0
- package/src/chat/context-breakdown.ts +11 -0
- package/src/chat/delta-coalescer.ts +8 -0
- package/src/chat/external-sessions.ts +28 -0
- package/src/chat/generated-image.ts +25 -0
- package/src/chat/index.ts +5 -0
- package/src/chat/input.ts +13 -0
- package/src/chat/json-rpc-transport.ts +28 -0
- package/src/chat/limit-resume.ts +13 -0
- package/src/chat/projector.ts +160 -4
- package/src/chat/readable-text.ts +3 -0
- package/src/chat/request-summary.ts +18 -4
- package/src/chat/subagent-projection.ts +3 -0
- package/src/chat/subagent-reader.ts +76 -0
- package/src/chat/subagent-settlement.ts +24 -0
- package/src/chat/thread-search.ts +13 -0
- package/src/chat/thread.ts +31 -0
- package/src/chat/ui-queries.ts +122 -0
- package/src/chat/ui-replies.ts +34 -0
- package/src/chat/visual-store.ts +49 -0
- package/src/chat/wake-chat.ts +3 -0
- package/src/client-sinks.ts +2 -0
- package/src/coded-error.ts +1 -5
- package/src/context/argv.ts +18 -2
- package/src/context/index.ts +1 -0
- package/src/context/refusal.ts +3 -0
- package/src/context/verb.ts +32 -4
- package/src/coordination/index.ts +10 -0
- package/src/error-text.ts +7 -0
- package/src/fields.ts +14 -0
- package/src/fs.ts +20 -4
- package/src/host/agent-host.ts +20 -0
- package/src/host/environment.ts +6 -0
- package/src/host/handlers.ts +18 -0
- package/src/host/memory-port.ts +4 -0
- package/src/host/wiring.ts +10 -0
- package/src/index.ts +1 -0
- package/src/lineage.ts +24 -5
- package/src/messages/deliver-message.ts +6 -0
- package/src/messages/deliver-notice.ts +13 -0
- package/src/messages/notice-store.ts +16 -4
- package/src/modes.ts +1 -0
- package/src/outbox/outbox-worker.ts +31 -0
- package/src/outbox/outbox.ts +5 -0
- package/src/providers/accounts/accounts.ts +36 -0
- package/src/providers/accounts/acp-account.ts +22 -27
- package/src/providers/accounts/launch.ts +4 -0
- package/src/providers/accounts/service.ts +110 -12
- package/src/providers/accounts/shadow-home.ts +10 -0
- package/src/providers/accounts/status.ts +13 -4
- package/src/providers/accounts/store.ts +25 -0
- package/src/providers/accounts/variables.ts +11 -1
- package/src/providers/acp-agent-state.ts +24 -3
- package/src/providers/acp-provider.ts +5 -0
- package/src/providers/acp-usage-log.ts +2 -0
- package/src/providers/catalog.ts +17 -0
- package/src/providers/claude.ts +10 -1
- package/src/providers/codex-models.json +3 -2
- package/src/providers/codex-rules.ts +3 -0
- package/src/providers/codex.ts +2 -1
- package/src/providers/detect.ts +6 -1
- package/src/providers/provider.ts +1 -1
- package/src/providers/registry.ts +18 -2
- package/src/providers/shell-commands.ts +37 -2
- package/src/record-directory.ts +19 -0
- package/src/run-process.ts +24 -3
- package/src/serializer.ts +3 -0
- package/src/skills.ts +73 -10
- package/src/tasks/background-limit.ts +9 -2
- package/src/tasks/end-children.ts +9 -0
- package/src/tasks/give-task.ts +8 -0
- package/src/tasks/task-coordinator.ts +81 -17
- package/src/tasks/task-store.ts +28 -0
- package/src/tasks/waiting-child.ts +21 -0
- package/src/tasks/wake-parent.ts +26 -0
- package/src/tasks/wiring.ts +22 -0
- package/src/testing/accounts.ts +2 -0
- package/src/testing/fake-acp-capture.ts +16 -0
- package/src/testing/fake-acp.ts +66 -0
- package/src/testing/fake-claude.ts +177 -0
- package/src/testing/fake-cli.ts +29 -0
- package/src/testing/fake-codex.ts +122 -0
- package/src/testing/manual-clock.ts +2 -0
- package/src/testing/watch.ts +7 -1
- package/src/title-file.ts +12 -0
- package/src/usage/accounts.ts +6 -0
- package/src/usage/aggregate.ts +19 -0
- package/src/usage/exchange.ts +12 -0
- package/src/usage/index-file.ts +27 -0
- package/src/usage/limits/monitor.ts +31 -3
- package/src/usage/limits/normalize.ts +30 -0
- package/src/usage/limits/probe.ts +17 -0
- package/src/usage/prices-snapshot.json +8 -0
- package/src/usage/pricing.ts +35 -0
- package/src/usage/projects.ts +8 -0
- package/src/usage/readers/claude.ts +12 -9
- package/src/usage/readers/codex.ts +40 -8
- package/src/usage/readers/grok.ts +17 -9
- package/src/usage/readers/host-log.ts +9 -9
- package/src/usage/record.ts +22 -0
- package/src/usage/roots.ts +6 -0
- package/src/usage/scanner.ts +61 -0
- package/src/usage/usage-service.ts +14 -0
- package/src/watch-seam.ts +8 -0
- package/src/windows-command.ts +12 -0
package/src/chat/chat-core.ts
CHANGED
|
@@ -76,6 +76,7 @@ type BookmarkableItem = Extract<ChatItem, { kind: 'user' | 'assistant' }>;
|
|
|
76
76
|
|
|
77
77
|
function withoutHidden(info: ChatInfo): ChatInfo {
|
|
78
78
|
const { hidden: _hidden, ...rest } = info;
|
|
79
|
+
|
|
79
80
|
return rest;
|
|
80
81
|
}
|
|
81
82
|
|
|
@@ -88,6 +89,7 @@ function isBookmarkable(item: ChatItem): item is BookmarkableItem {
|
|
|
88
89
|
function excerptOf(item: BookmarkableItem): string {
|
|
89
90
|
const files = item.kind === 'user' ? (item.attachments ?? []).map((attachment) => attachment.name).join(', ') : '';
|
|
90
91
|
const text = item.kind === 'assistant' ? readableAssistantText(item) : item.text;
|
|
92
|
+
|
|
91
93
|
return (text.trim() === '' ? files : text).replace(/\s+/g, ' ').trim();
|
|
92
94
|
}
|
|
93
95
|
|
|
@@ -176,16 +178,13 @@ export interface ChatCoreOptions {
|
|
|
176
178
|
env?: Record<string, string | undefined>;
|
|
177
179
|
// What the agent is told once, at the start of every process, when the host has one note for every chat.
|
|
178
180
|
instructions?: string;
|
|
179
|
-
// The CLIs to run, when they are not the ones the providers name
|
|
181
|
+
// The CLIs to run, when they are not the ones the providers name.
|
|
180
182
|
command?: string[];
|
|
181
183
|
codexCommand?: string[];
|
|
182
|
-
// How a CLI is started; a test runs a fake in the same process instead of spawning one.
|
|
183
184
|
spawn?: SpawnChatProcess;
|
|
184
185
|
// How the host names itself to an app-server it opens for a question about a thread.
|
|
185
186
|
codexClient?: CodexClientInfo;
|
|
186
|
-
// Where the skill folders are looked for; a test points it at a temporary tree.
|
|
187
187
|
skills?: SkillIndex;
|
|
188
|
-
// Where the files people attach are written.
|
|
189
188
|
attachments: AttachmentStore;
|
|
190
189
|
bookmarks?: BookmarkStore;
|
|
191
190
|
// Pages agents publish in a chat; their files go in the same attachment folders.
|
|
@@ -196,7 +195,7 @@ export interface ChatCoreOptions {
|
|
|
196
195
|
claudeTitles?: { forSession(agentSessionId: string, projectsDir: string, cwd: string): Promise<string | null> };
|
|
197
196
|
// A name for a Codex chat, asked of a one-shot CLI: its app-server names no thread on its own.
|
|
198
197
|
nameChat?: (provider: AgentKind, input: ChatTitleInput) => Promise<string | null>;
|
|
199
|
-
// Where a subagent's whole conversation is read and how its growth is noticed
|
|
198
|
+
// Where a subagent's whole conversation is read and how its growth is noticed.
|
|
200
199
|
subagents?: Partial<Pick<SubagentReaderOptions, 'claudeProjectsDir' | 'seams' | 'now' | 'listOnce'>>;
|
|
201
200
|
/*
|
|
202
201
|
* Asked when a chat is loaded with a turn that could be resumed: true once the resume is owed,
|
|
@@ -291,28 +290,36 @@ export class ChatCore {
|
|
|
291
290
|
this.onInterruptedRun = options.onInterruptedRun ?? null;
|
|
292
291
|
this.limitResume = options.limitResume ?? null;
|
|
293
292
|
this.hooks = options.hooks ?? {};
|
|
293
|
+
|
|
294
294
|
this.commands = {
|
|
295
295
|
...(options.command ? { claude: options.command } : {}),
|
|
296
296
|
...(options.codexCommand ? { codex: options.codexCommand } : {})
|
|
297
297
|
};
|
|
298
|
+
|
|
298
299
|
this.spawn = options.spawn ?? null;
|
|
299
300
|
this.codexClient = options.codexClient ?? null;
|
|
300
301
|
this.env = definedEnv(options.env ?? process.env);
|
|
302
|
+
|
|
301
303
|
// 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.
|
|
302
304
|
this.bookmarks?.listen((chatId, bookmarks) => {
|
|
303
305
|
this.listChanged(chatId);
|
|
306
|
+
|
|
304
307
|
for (const clientId of this.attached.get(chatId) ?? []) {
|
|
305
308
|
this.sinks.to(clientId, { event: 'chat.bookmarks', payload: { chatId, bookmarks } });
|
|
306
309
|
}
|
|
307
310
|
});
|
|
311
|
+
|
|
308
312
|
this.visuals?.listen((chatId, visuals) => {
|
|
309
313
|
this.listChanged(chatId);
|
|
314
|
+
|
|
310
315
|
for (const clientId of this.attached.get(chatId) ?? []) {
|
|
311
316
|
this.sinks.to(clientId, { event: 'chat.visuals', payload: { chatId, visuals } });
|
|
312
317
|
}
|
|
313
318
|
});
|
|
319
|
+
|
|
314
320
|
this.claudeProjectsDir =
|
|
315
321
|
options.subagents?.claudeProjectsDir ?? usageRoots(options.env ?? process.env).find((root) => root.provider === 'claude')?.path ?? '';
|
|
322
|
+
|
|
316
323
|
this.subagents = new SubagentReader({
|
|
317
324
|
...options.subagents,
|
|
318
325
|
claudeProjectsDir: this.claudeProjectsDir,
|
|
@@ -320,6 +327,7 @@ export class ChatCore {
|
|
|
320
327
|
chatInfo: async (chatId) => (await this.readChat(chatId))?.info ?? null,
|
|
321
328
|
chat: (chatId) => {
|
|
322
329
|
const session = this.chats.get(chatId);
|
|
330
|
+
|
|
323
331
|
return session
|
|
324
332
|
? {
|
|
325
333
|
info: session.info,
|
|
@@ -351,15 +359,17 @@ export class ChatCore {
|
|
|
351
359
|
if (isDefaultAccountOf('claude', info.account)) {
|
|
352
360
|
return this.claudeProjectsDir;
|
|
353
361
|
}
|
|
362
|
+
|
|
354
363
|
const folder = this.accounts?.transcriptFolder('claude', info.account) ?? null;
|
|
364
|
+
|
|
355
365
|
return folder === null ? '' : join(folder, 'projects');
|
|
356
366
|
}
|
|
357
367
|
|
|
358
|
-
/* Whether a conversation of one account of this CLI can go on under the other. */
|
|
359
368
|
canContinue(kind: AgentKind, from: string | undefined, to: string | undefined): boolean {
|
|
360
369
|
if ((from ?? kind) === (to ?? kind)) {
|
|
361
370
|
return true;
|
|
362
371
|
}
|
|
372
|
+
|
|
363
373
|
return this.accounts?.canContinue(kind, from, to) ?? false;
|
|
364
374
|
}
|
|
365
375
|
|
|
@@ -368,7 +378,6 @@ export class ChatCore {
|
|
|
368
378
|
launchEnv(this.accounts, kind, account, this.env);
|
|
369
379
|
}
|
|
370
380
|
|
|
371
|
-
/* What a person calls an account of this CLI. */
|
|
372
381
|
accountLabel(kind: AgentKind, account: string | undefined): string {
|
|
373
382
|
return isDefaultAccountOf(kind, account) || account === undefined ? this.providers.get(kind).name : (this.accounts?.labelOf(account) ?? account);
|
|
374
383
|
}
|
|
@@ -377,10 +386,13 @@ export class ChatCore {
|
|
|
377
386
|
async readChat(chatId: string): Promise<{ info: ChatInfo; items: ChatItem[] } | null> {
|
|
378
387
|
await this.loaded(chatId);
|
|
379
388
|
const session = this.chats.get(chatId);
|
|
389
|
+
|
|
380
390
|
if (session) {
|
|
381
391
|
return session.thread.snapshot();
|
|
382
392
|
}
|
|
393
|
+
|
|
383
394
|
const stored = await this.store?.read(chatId);
|
|
395
|
+
|
|
384
396
|
return stored ? { info: stored.info, items: stored.items } : null;
|
|
385
397
|
}
|
|
386
398
|
|
|
@@ -389,6 +401,7 @@ export class ChatCore {
|
|
|
389
401
|
const catalog = this.providers.get(provider).catalog;
|
|
390
402
|
const preference = this.composerPreferences.for(provider);
|
|
391
403
|
const normalized = catalog.normalize(selection ?? preference.selection);
|
|
404
|
+
|
|
392
405
|
return {
|
|
393
406
|
selection: normalized,
|
|
394
407
|
...(preference.runtimeMode ? { runtimeMode: preference.runtimeMode } : {}),
|
|
@@ -402,6 +415,7 @@ export class ChatCore {
|
|
|
402
415
|
*/
|
|
403
416
|
observe(sink: AgentSink): () => void {
|
|
404
417
|
this.observers.add(sink);
|
|
418
|
+
|
|
405
419
|
return () => {
|
|
406
420
|
this.observers.delete(sink);
|
|
407
421
|
};
|
|
@@ -420,23 +434,30 @@ export class ChatCore {
|
|
|
420
434
|
create(payload: ChatCreatePayload): Promise<ChatInfo> {
|
|
421
435
|
// 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.
|
|
422
436
|
const inFlight = this.creating.get(payload.chatId);
|
|
437
|
+
|
|
423
438
|
if (inFlight) {
|
|
424
439
|
return inFlight;
|
|
425
440
|
}
|
|
441
|
+
|
|
426
442
|
const existing = this.chats.get(payload.chatId);
|
|
443
|
+
|
|
427
444
|
if (existing) {
|
|
428
445
|
return Promise.resolve(existing.info);
|
|
429
446
|
}
|
|
447
|
+
|
|
430
448
|
const created = this.createNow(payload).finally(() => this.creating.delete(payload.chatId));
|
|
431
449
|
this.creating.set(payload.chatId, created);
|
|
450
|
+
|
|
432
451
|
return created;
|
|
433
452
|
}
|
|
434
453
|
|
|
435
454
|
private async createNow(payload: ChatCreatePayload): Promise<ChatInfo> {
|
|
436
455
|
const stored = await this.store?.read(payload.chatId);
|
|
456
|
+
|
|
437
457
|
if (!stored && (await this.store?.unreadable(payload.chatId))) {
|
|
438
458
|
throw new ChatError('chat-unreadable', `The record of chat ${payload.chatId} cannot be read by this version, so it is left as it is`);
|
|
439
459
|
}
|
|
460
|
+
|
|
440
461
|
// A thread on disk keeps its provider; the selection it stored only makes sense in that catalog.
|
|
441
462
|
const kind = stored?.info.provider ?? payload.provider ?? 'claude';
|
|
442
463
|
const provider = this.providers.get(kind);
|
|
@@ -448,13 +469,18 @@ export class ChatCore {
|
|
|
448
469
|
// A new one without an account takes the person's pick for this CLI, which is refused rather than replaced when it went.
|
|
449
470
|
// The pick is checked as named, before the default account loses its id, so one that is turned off is refused too.
|
|
450
471
|
const requested = payload.account ?? this.composerPreferences.for(kind).account;
|
|
472
|
+
|
|
451
473
|
if (!stored) {
|
|
452
474
|
this.requireAccount(kind, requested);
|
|
453
475
|
}
|
|
476
|
+
|
|
454
477
|
const account = stored ? stored.info.account : storedAccount(kind, requested);
|
|
478
|
+
|
|
455
479
|
const narrowed = (mode: RuntimeMode): RuntimeMode => (this.hooks.runtimeModeFor ? this.hooks.runtimeModeFor(payload.chatId, mode) : mode);
|
|
480
|
+
|
|
456
481
|
// Asked again for a chat on disk, since the host may have listed it since.
|
|
457
482
|
const hidden = this.hooks.hiddenFor?.(payload.chatId) ? { hidden: true } : {};
|
|
483
|
+
|
|
458
484
|
const info: ChatInfo = stored?.info
|
|
459
485
|
? { ...withoutHidden(stored.info), runtimeMode: narrowed(stored.info.runtimeMode), ...hidden }
|
|
460
486
|
: {
|
|
@@ -474,6 +500,7 @@ export class ChatCore {
|
|
|
474
500
|
usage: { contextTokens: 0, contextWindow: catalog.contextWindowFor(selection), costUsd: 0, turns: 0 },
|
|
475
501
|
createdAt: Date.now()
|
|
476
502
|
};
|
|
503
|
+
|
|
477
504
|
const resume: ResumeDecision = stored ? await this.owedResume(payload.chatId, stored) : { resumeTurnId: null, reason: null };
|
|
478
505
|
const claudeTitles = this.claudeTitles;
|
|
479
506
|
const nameChat = this.nameChat;
|
|
@@ -481,6 +508,7 @@ export class ChatCore {
|
|
|
481
508
|
this.logs.set(payload.chatId, await this.openLog(payload.chatId, stored ?? null));
|
|
482
509
|
const promptNotes = this.hooks.promptNotesFor?.(payload.chatId);
|
|
483
510
|
const references = this.hooks.referencesFor?.(payload.chatId);
|
|
511
|
+
|
|
484
512
|
const session = new ChatSession({
|
|
485
513
|
info,
|
|
486
514
|
...(this.uiFenceLanguage ? { uiFenceLanguage: this.uiFenceLanguage } : {}),
|
|
@@ -494,6 +522,7 @@ export class ChatCore {
|
|
|
494
522
|
env: (chatAccount) => {
|
|
495
523
|
const env = definedEnv(launchEnv(this.accounts, kind, chatAccount, base));
|
|
496
524
|
this.accounts?.launched?.(kind, chatAccount);
|
|
525
|
+
|
|
497
526
|
return env;
|
|
498
527
|
},
|
|
499
528
|
saveGeneratedImage: (ref, upload) => this.attachments.saveGenerated(payload.chatId, ref, upload),
|
|
@@ -517,24 +546,31 @@ export class ChatCore {
|
|
|
517
546
|
...(kind === 'claude' ? { subagentSettlement: (toolUseId: string) => this.subagents.claudeSettlement(payload.chatId, toolUseId) } : {}),
|
|
518
547
|
...(this.limitResume ? { limitResume: limitHooks(this.limitResume, payload.chatId) } : {})
|
|
519
548
|
});
|
|
549
|
+
|
|
520
550
|
this.chats.set(session.id, session);
|
|
551
|
+
|
|
521
552
|
if (stored) {
|
|
522
553
|
session.settleStored(selection, resume);
|
|
523
554
|
session.settleOwedResume();
|
|
524
555
|
await session.settleOrphanedSubagents();
|
|
525
556
|
}
|
|
557
|
+
|
|
526
558
|
await this.hooks.opened?.(session, stored ?? null);
|
|
559
|
+
|
|
527
560
|
return session.info;
|
|
528
561
|
}
|
|
529
562
|
|
|
530
563
|
async storedBackground(chatId: string): Promise<{ turnId: string | null; items: BackgroundWork[]; commands: string[]; goal: boolean }> {
|
|
531
564
|
const record = await this.store?.read(chatId);
|
|
532
565
|
const items = record?.items ?? [];
|
|
566
|
+
|
|
533
567
|
const background = items.filter(
|
|
534
568
|
(item): item is BackgroundWork =>
|
|
535
569
|
isBackgroundWork(item) && (runsInBackground(item) || items.some((note) => note.id === `lost-background-${item.id}`))
|
|
536
570
|
);
|
|
571
|
+
|
|
537
572
|
const last = items.findLast((item) => item.kind === 'turn');
|
|
573
|
+
|
|
538
574
|
return {
|
|
539
575
|
turnId: background.at(-1)?.turnId ?? last?.id ?? null,
|
|
540
576
|
items: background,
|
|
@@ -553,10 +589,12 @@ export class ChatCore {
|
|
|
553
589
|
|
|
554
590
|
configure(payload: ChatConfigurePayload): ChatInfo {
|
|
555
591
|
const session = this.require(payload.chatId);
|
|
592
|
+
|
|
556
593
|
if (payload.account !== undefined) {
|
|
557
594
|
this.refuseWhileRelocating(payload.chatId);
|
|
558
595
|
this.switchAccount(session, payload.account);
|
|
559
596
|
}
|
|
597
|
+
|
|
560
598
|
return session.configure(payload);
|
|
561
599
|
}
|
|
562
600
|
|
|
@@ -567,19 +605,24 @@ export class ChatCore {
|
|
|
567
605
|
protected switchAccount(session: ChatSession, requested: string): void {
|
|
568
606
|
const { provider: kind, account: from } = session.info;
|
|
569
607
|
const to = storedAccount(kind, requested);
|
|
608
|
+
|
|
570
609
|
if (to === from) {
|
|
571
610
|
return;
|
|
572
611
|
}
|
|
612
|
+
|
|
573
613
|
// As named, so a switch to the default account is refused while it is turned off.
|
|
574
614
|
this.requireAccount(kind, requested);
|
|
575
615
|
const spoke = session.info.agentSessionId !== null || session.info.usage.turns > 0;
|
|
616
|
+
|
|
576
617
|
if (spoke && !this.canContinue(kind, from, to)) {
|
|
577
618
|
const label = this.accountLabel(kind, to);
|
|
619
|
+
|
|
578
620
|
throw new AccountError(
|
|
579
621
|
'account-incompatible',
|
|
580
622
|
`This conversation is kept by the account '${this.accountLabel(kind, from)}', which '${label}' cannot read. Fork the chat to go on under '${label}'.`
|
|
581
623
|
);
|
|
582
624
|
}
|
|
625
|
+
|
|
583
626
|
session.setAccount(to);
|
|
584
627
|
}
|
|
585
628
|
|
|
@@ -590,24 +633,31 @@ export class ChatCore {
|
|
|
590
633
|
attach(chatId: string, clientId: string, historyLimit?: number, since?: number): ChatAttachResult {
|
|
591
634
|
const session = this.require(chatId);
|
|
592
635
|
let clients = this.attached.get(chatId);
|
|
636
|
+
|
|
593
637
|
if (!clients) {
|
|
594
638
|
clients = new Set();
|
|
595
639
|
this.attached.set(chatId, clients);
|
|
596
640
|
}
|
|
641
|
+
|
|
597
642
|
// The snapshot already holds the text of a delta that is waiting, so it goes out before this
|
|
598
643
|
// client is on the list: the text is either in the snapshot or in the stream, never both.
|
|
599
644
|
this.coalescers.get(chatId)?.flush();
|
|
600
645
|
clients.add(clientId);
|
|
601
646
|
const log = this.logs.get(chatId)!;
|
|
602
647
|
const events = since === undefined ? null : log.after(since);
|
|
648
|
+
|
|
603
649
|
if (events !== null) {
|
|
604
650
|
return { info: session.info, items: [], events: [...events, ...session.uiPreviews], seq: log.seq };
|
|
605
651
|
}
|
|
652
|
+
|
|
606
653
|
if (historyLimit === undefined) {
|
|
607
654
|
const snapshot = session.thread.snapshot();
|
|
655
|
+
|
|
608
656
|
return { ...snapshot, info: session.info, items: session.previewItems(snapshot.items), seq: log.seq };
|
|
609
657
|
}
|
|
658
|
+
|
|
610
659
|
const page = session.thread.history(historyLimit);
|
|
660
|
+
|
|
611
661
|
return {
|
|
612
662
|
...page,
|
|
613
663
|
info: session.info,
|
|
@@ -627,25 +677,34 @@ export class ChatCore {
|
|
|
627
677
|
async attachWithBookmarks(chatId: string, clientId: string, historyLimit?: number, since?: number): Promise<ChatAttachResult> {
|
|
628
678
|
// A client that only watches a chat sends no create, so a chat on disk nobody loaded since the host started is loaded for it.
|
|
629
679
|
await this.loaded(chatId);
|
|
680
|
+
|
|
630
681
|
if (!this.chats.has(chatId) && (await this.store?.has(chatId)) === true) {
|
|
631
682
|
await this.create({ chatId });
|
|
632
683
|
}
|
|
684
|
+
|
|
633
685
|
this.require(chatId);
|
|
686
|
+
|
|
634
687
|
const unreadable = (what: string) => (e: unknown) => {
|
|
635
688
|
console.error(`The ${what} of chat ${chatId} could not be read:`, errorText(e));
|
|
689
|
+
|
|
636
690
|
return undefined;
|
|
637
691
|
};
|
|
692
|
+
|
|
638
693
|
let bookmarks: ChatBookmark[] | undefined;
|
|
639
694
|
let visuals: ChatVisual[] | undefined;
|
|
640
695
|
let changes: number | undefined;
|
|
696
|
+
|
|
641
697
|
do {
|
|
642
698
|
changes = this.listChanges.get(chatId);
|
|
699
|
+
|
|
643
700
|
[bookmarks, visuals] = await Promise.all([
|
|
644
701
|
this.bookmarks?.read(chatId).catch(unreadable('bookmarks')),
|
|
645
702
|
this.visuals?.list(chatId).catch(unreadable('visuals'))
|
|
646
703
|
]);
|
|
647
704
|
} while (this.listChanges.get(chatId) !== changes);
|
|
705
|
+
|
|
648
706
|
const result = this.attach(chatId, clientId, historyLimit, since);
|
|
707
|
+
|
|
649
708
|
return { ...result, ...(bookmarks === undefined ? {} : { bookmarks }), ...(visuals === undefined ? {} : { visuals }) };
|
|
650
709
|
}
|
|
651
710
|
|
|
@@ -656,19 +715,23 @@ export class ChatCore {
|
|
|
656
715
|
/* Marks a message of the chat's own thread; a message that already has one keeps it. */
|
|
657
716
|
addBookmark(chatId: string, itemId: string, name?: string): Promise<ChatBookmark[]> {
|
|
658
717
|
const item = this.require(chatId).thread.get(itemId);
|
|
718
|
+
|
|
659
719
|
if (!item || !isBookmarkable(item)) {
|
|
660
720
|
throw new ChatError('item-not-found', `Chat ${chatId} has no message ${itemId} to bookmark`);
|
|
661
721
|
}
|
|
722
|
+
|
|
662
723
|
return this.requireBookmarks().add(chatId, { itemId, excerpt: excerptOf(item), ...(name === undefined ? {} : { name }) }, Date.now());
|
|
663
724
|
}
|
|
664
725
|
|
|
665
726
|
renameBookmark(chatId: string, itemId: string, name: string): Promise<ChatBookmark[]> {
|
|
666
727
|
this.require(chatId);
|
|
728
|
+
|
|
667
729
|
return this.requireBookmarks().rename(chatId, itemId, name);
|
|
668
730
|
}
|
|
669
731
|
|
|
670
732
|
removeBookmark(chatId: string, itemId: string): Promise<ChatBookmark[]> {
|
|
671
733
|
this.require(chatId);
|
|
734
|
+
|
|
672
735
|
return this.requireBookmarks().remove(chatId, itemId);
|
|
673
736
|
}
|
|
674
737
|
|
|
@@ -676,6 +739,7 @@ export class ChatCore {
|
|
|
676
739
|
if (!this.bookmarks) {
|
|
677
740
|
throw new ChatError('chat-unsupported', 'This host keeps no bookmarks');
|
|
678
741
|
}
|
|
742
|
+
|
|
679
743
|
return this.bookmarks;
|
|
680
744
|
}
|
|
681
745
|
|
|
@@ -686,10 +750,13 @@ export class ChatCore {
|
|
|
686
750
|
async publishVisual(chatId: string, input: VisualInput): Promise<ChatVisual> {
|
|
687
751
|
const visuals = this.requireVisuals();
|
|
688
752
|
await this.loaded(chatId);
|
|
753
|
+
|
|
689
754
|
if (!(await this.hasStored(chatId))) {
|
|
690
755
|
throw new ChatError('chat-not-found', `No chat ${chatId}`);
|
|
691
756
|
}
|
|
757
|
+
|
|
692
758
|
const turnId = input.turnId ?? this.chats.get(chatId)?.info.activeTurnId ?? undefined;
|
|
759
|
+
|
|
693
760
|
return visuals.publish(chatId, turnId === undefined ? input : { ...input, turnId });
|
|
694
761
|
}
|
|
695
762
|
|
|
@@ -700,6 +767,7 @@ export class ChatCore {
|
|
|
700
767
|
|
|
701
768
|
removeVisual(chatId: string, visualId: string): Promise<ChatVisual[]> {
|
|
702
769
|
this.require(chatId);
|
|
770
|
+
|
|
703
771
|
return this.requireVisuals().remove(chatId, visualId);
|
|
704
772
|
}
|
|
705
773
|
|
|
@@ -707,6 +775,7 @@ export class ChatCore {
|
|
|
707
775
|
if (!this.visuals) {
|
|
708
776
|
throw new ChatError('chat-unsupported', 'This host keeps no visuals');
|
|
709
777
|
}
|
|
778
|
+
|
|
710
779
|
return this.visuals;
|
|
711
780
|
}
|
|
712
781
|
|
|
@@ -714,6 +783,7 @@ export class ChatCore {
|
|
|
714
783
|
history(chatId: string, cursor?: string, limit?: number): ChatHistoryResult {
|
|
715
784
|
const session = this.require(chatId);
|
|
716
785
|
this.coalescers.get(chatId)?.flush();
|
|
786
|
+
|
|
717
787
|
return session.thread.history(limit, cursor);
|
|
718
788
|
}
|
|
719
789
|
|
|
@@ -729,6 +799,7 @@ export class ChatCore {
|
|
|
729
799
|
clients.delete(clientId);
|
|
730
800
|
}
|
|
731
801
|
}
|
|
802
|
+
|
|
732
803
|
this.subagents.releaseClient(clientId);
|
|
733
804
|
}
|
|
734
805
|
|
|
@@ -741,11 +812,14 @@ export class ChatCore {
|
|
|
741
812
|
if (payload.watch === false) {
|
|
742
813
|
this.subagents.release(clientId, payload.chatId, payload.toolUseId);
|
|
743
814
|
}
|
|
815
|
+
|
|
744
816
|
const result = await this.subagents.read(payload.chatId, payload.toolUseId, payload.cursor, payload.limit);
|
|
745
817
|
await this.chats.get(payload.chatId)?.recheckOrphan(payload.toolUseId);
|
|
818
|
+
|
|
746
819
|
if (payload.watch === true) {
|
|
747
820
|
this.subagents.hold(clientId, payload.chatId, payload.toolUseId);
|
|
748
821
|
}
|
|
822
|
+
|
|
749
823
|
return result;
|
|
750
824
|
}
|
|
751
825
|
|
|
@@ -758,15 +832,19 @@ export class ChatCore {
|
|
|
758
832
|
await this.loaded(chatId);
|
|
759
833
|
const session = this.require(chatId);
|
|
760
834
|
const row = session.thread.find('subagent', (item) => item.toolUseId === toolUseId);
|
|
835
|
+
|
|
761
836
|
if (!row) {
|
|
762
837
|
throw new ChatError('subagent-not-found', 'This chat has no such sub-agent');
|
|
763
838
|
}
|
|
839
|
+
|
|
764
840
|
if (row.status !== 'running') {
|
|
765
841
|
return;
|
|
766
842
|
}
|
|
843
|
+
|
|
767
844
|
if (row.origin === HOST_TASK_ORIGIN) {
|
|
768
845
|
throw new ChatError('chat-unsupported', 'This row stands for work the host runs; stop it there');
|
|
769
846
|
}
|
|
847
|
+
|
|
770
848
|
session.stopSubagent(toolUseId);
|
|
771
849
|
}
|
|
772
850
|
|
|
@@ -800,30 +878,40 @@ export class ChatCore {
|
|
|
800
878
|
if (!this.store) {
|
|
801
879
|
throw new ChatError('chat-unsupported', 'This host keeps no chat records');
|
|
802
880
|
}
|
|
881
|
+
|
|
803
882
|
if (this.chats.has(chatId) || this.creating.has(chatId)) {
|
|
804
883
|
throw new ChatError('chat-busy', `Chat ${chatId} is already loaded`);
|
|
805
884
|
}
|
|
885
|
+
|
|
806
886
|
const copies = new Map<string, Promise<ChatAttachment>>();
|
|
887
|
+
|
|
807
888
|
const copy = (attachment: ChatAttachment): Promise<ChatAttachment> => {
|
|
808
889
|
let pending = copies.get(attachment.id);
|
|
890
|
+
|
|
809
891
|
if (!pending) {
|
|
810
892
|
pending = this.attachments.copy(chatId, attachment);
|
|
811
893
|
copies.set(attachment.id, pending);
|
|
812
894
|
}
|
|
895
|
+
|
|
813
896
|
return pending;
|
|
814
897
|
};
|
|
898
|
+
|
|
815
899
|
const copied = await Promise.all(
|
|
816
900
|
items.map(async (item): Promise<ChatItem> => {
|
|
817
901
|
if (item.kind === 'user' && item.attachments?.length) {
|
|
818
902
|
return { ...item, attachments: await Promise.all(item.attachments.map(copy)) };
|
|
819
903
|
}
|
|
904
|
+
|
|
820
905
|
const generated = generatedImageAttachment(item);
|
|
906
|
+
|
|
821
907
|
if (item.kind === 'tool' && generated) {
|
|
822
908
|
return { ...item, input: { ...(item.input as Record<string, unknown>), attachment: await copy(generated) } };
|
|
823
909
|
}
|
|
910
|
+
|
|
824
911
|
return item;
|
|
825
912
|
})
|
|
826
913
|
);
|
|
914
|
+
|
|
827
915
|
await this.store.write(chatId, info, copied, { seq: 0, resetSeq: 0 }, preambles);
|
|
828
916
|
}
|
|
829
917
|
|
|
@@ -835,34 +923,42 @@ export class ChatCore {
|
|
|
835
923
|
if (this.chats.has(chatId) || this.creating.has(chatId)) {
|
|
836
924
|
return false;
|
|
837
925
|
}
|
|
926
|
+
|
|
838
927
|
await Promise.all([
|
|
839
928
|
this.store?.delete(chatId),
|
|
840
929
|
this.attachments.removeAll(chatId),
|
|
841
930
|
this.bookmarks?.removeChat(chatId),
|
|
842
931
|
this.visuals?.removeChat(chatId)
|
|
843
932
|
]);
|
|
933
|
+
|
|
844
934
|
return true;
|
|
845
935
|
}
|
|
846
936
|
|
|
847
937
|
/* The turn a chat took last, read off the record without loading a chat nobody has; null before its first. */
|
|
848
938
|
async lastTurn(chatId: string): Promise<ChatTurnItem | null> {
|
|
849
939
|
const session = this.chats.get(chatId);
|
|
940
|
+
|
|
850
941
|
if (session) {
|
|
851
942
|
return session.thread.find('turn', () => true) ?? null;
|
|
852
943
|
}
|
|
944
|
+
|
|
853
945
|
const stored = await this.store?.read(chatId);
|
|
946
|
+
|
|
854
947
|
return stored?.items.findLast((item): item is ChatTurnItem => item.kind === 'turn') ?? null;
|
|
855
948
|
}
|
|
856
949
|
|
|
857
950
|
/* A line from the host in a chat's thread, loading the chat when nobody has; nothing for a chat that is gone. */
|
|
858
951
|
async addNote(chatId: string, level: 'info' | 'warning' | 'error', text: string): Promise<void> {
|
|
859
952
|
await this.loaded(chatId);
|
|
953
|
+
|
|
860
954
|
if (!(await this.hasStored(chatId))) {
|
|
861
955
|
return;
|
|
862
956
|
}
|
|
957
|
+
|
|
863
958
|
if (!this.chats.has(chatId)) {
|
|
864
959
|
await this.create({ chatId });
|
|
865
960
|
}
|
|
961
|
+
|
|
866
962
|
this.chats.get(chatId)?.addNote(level, text);
|
|
867
963
|
}
|
|
868
964
|
|
|
@@ -872,12 +968,15 @@ export class ChatCore {
|
|
|
872
968
|
*/
|
|
873
969
|
async deliverNote(chatId: string, delivery: { noteId: string; note: string; from?: string; preamble: string }): Promise<boolean> {
|
|
874
970
|
await this.loaded(chatId);
|
|
971
|
+
|
|
875
972
|
if (!(await this.hasStored(chatId))) {
|
|
876
973
|
return false;
|
|
877
974
|
}
|
|
975
|
+
|
|
878
976
|
if (!this.chats.has(chatId)) {
|
|
879
977
|
await this.create({ chatId });
|
|
880
978
|
}
|
|
979
|
+
|
|
881
980
|
return this.require(chatId).deliverNote(delivery);
|
|
882
981
|
}
|
|
883
982
|
|
|
@@ -888,10 +987,13 @@ export class ChatCore {
|
|
|
888
987
|
async send(chatId: string, text: string, extras: ChatSendExtras = {}, uploads: ChatAttachmentUpload[] = []): Promise<{ queued: boolean; turnId: string }> {
|
|
889
988
|
const session = this.require(chatId);
|
|
890
989
|
const checked = ChatAttachmentUploadsSchema.safeParse(uploads);
|
|
990
|
+
|
|
891
991
|
if (!checked.success) {
|
|
892
992
|
throw new ChatError('invalid-attachments', checked.error.issues[0]!.message);
|
|
893
993
|
}
|
|
994
|
+
|
|
894
995
|
const attachments = await Promise.all(uploads.map((upload) => this.attachments.save(chatId, upload)));
|
|
996
|
+
|
|
895
997
|
return session.sendInput(text, { ...extras, ...(attachments.length > 0 ? { attachments } : {}) });
|
|
896
998
|
}
|
|
897
999
|
|
|
@@ -899,6 +1001,7 @@ export class ChatCore {
|
|
|
899
1001
|
if (!this.attached.get(payload.chatId)?.has(clientId) || !this.uiQueries) {
|
|
900
1002
|
throw new ChatError('refused-query', 'Attach to the chat before resolving its UI links.');
|
|
901
1003
|
}
|
|
1004
|
+
|
|
902
1005
|
return this.uiQueries.link(this.require(payload.chatId), payload);
|
|
903
1006
|
}
|
|
904
1007
|
|
|
@@ -906,9 +1009,11 @@ export class ChatCore {
|
|
|
906
1009
|
if (!this.attached.get(payload.chatId)?.has(clientId)) {
|
|
907
1010
|
throw new ChatError('refused-query', 'Attach to the chat before reading its UI queries.');
|
|
908
1011
|
}
|
|
1012
|
+
|
|
909
1013
|
if (!this.uiQueries) {
|
|
910
1014
|
throw new ChatError('refused-query', 'This host does not provide UI query sources.');
|
|
911
1015
|
}
|
|
1016
|
+
|
|
912
1017
|
return this.uiQueries.query(this.require(payload.chatId), payload);
|
|
913
1018
|
}
|
|
914
1019
|
|
|
@@ -916,40 +1021,50 @@ export class ChatCore {
|
|
|
916
1021
|
if (!this.attached.get(payload.chatId)?.has(clientId)) {
|
|
917
1022
|
throw new ChatError('refused-query', 'Attach to the chat before answering its UI.');
|
|
918
1023
|
}
|
|
1024
|
+
|
|
919
1025
|
const session = this.require(payload.chatId);
|
|
920
1026
|
const queries = await this.uiQueries?.choiceValues(session, payload);
|
|
921
1027
|
const result = session.choose(payload, queries);
|
|
922
1028
|
await this.persisted(payload.chatId);
|
|
1029
|
+
|
|
923
1030
|
return result;
|
|
924
1031
|
}
|
|
925
1032
|
|
|
926
1033
|
/* The file behind an attachment id: what this chat's thread or queue says it is, or a visual's page. */
|
|
927
1034
|
attachment(chatId: string, id: string): ChatAttachment | null {
|
|
928
1035
|
const session = this.chats.get(chatId);
|
|
1036
|
+
|
|
929
1037
|
if (!session) {
|
|
930
1038
|
return null;
|
|
931
1039
|
}
|
|
1040
|
+
|
|
932
1041
|
for (const item of session.thread.list()) {
|
|
933
1042
|
const generated = generatedImageAttachment(item);
|
|
934
1043
|
const found = item.kind === 'user' ? item.attachments?.find((attachment) => attachment.id === id) : generated?.id === id ? generated : null;
|
|
1044
|
+
|
|
935
1045
|
if (found) {
|
|
936
1046
|
return found;
|
|
937
1047
|
}
|
|
938
1048
|
}
|
|
1049
|
+
|
|
939
1050
|
for (const message of session.info.queue ?? []) {
|
|
940
1051
|
const found = message.attachments?.find((attachment) => attachment.id === id);
|
|
1052
|
+
|
|
941
1053
|
if (found) {
|
|
942
1054
|
return found;
|
|
943
1055
|
}
|
|
944
1056
|
}
|
|
1057
|
+
|
|
945
1058
|
return this.visuals?.attachment(chatId, id) ?? null;
|
|
946
1059
|
}
|
|
947
1060
|
|
|
948
1061
|
unqueue(chatId: string, messageId: string): ChatQueuedMessage {
|
|
949
1062
|
const message = this.require(chatId).unqueue(messageId);
|
|
1063
|
+
|
|
950
1064
|
if (!message) {
|
|
951
1065
|
throw new ChatError('request-not-found', `No queued message ${messageId} in chat ${chatId}`);
|
|
952
1066
|
}
|
|
1067
|
+
|
|
953
1068
|
return message;
|
|
954
1069
|
}
|
|
955
1070
|
|
|
@@ -962,6 +1077,7 @@ export class ChatCore {
|
|
|
962
1077
|
/* What the chat's CLI would run as a skill. */
|
|
963
1078
|
skills(chatId: string): Promise<ChatSkill[]> {
|
|
964
1079
|
const session = this.require(chatId);
|
|
1080
|
+
|
|
965
1081
|
return session.skills(() => this.skillIndex.list(session.info.provider, session.info.cwd));
|
|
966
1082
|
}
|
|
967
1083
|
|
|
@@ -982,9 +1098,11 @@ export class ChatCore {
|
|
|
982
1098
|
compact(chatId: string): void {
|
|
983
1099
|
const session = this.require(chatId);
|
|
984
1100
|
this.refuseWhileRelocating(chatId);
|
|
1101
|
+
|
|
985
1102
|
if (session.busy) {
|
|
986
1103
|
throw new ChatError('chat-busy', `Chat ${chatId} is still working on the previous message`);
|
|
987
1104
|
}
|
|
1105
|
+
|
|
988
1106
|
session.compact();
|
|
989
1107
|
}
|
|
990
1108
|
|
|
@@ -992,15 +1110,18 @@ export class ChatCore {
|
|
|
992
1110
|
async clear(chatId: string, force = false): Promise<void> {
|
|
993
1111
|
const session = this.require(chatId);
|
|
994
1112
|
this.refuseWhileRelocating(chatId);
|
|
1113
|
+
|
|
995
1114
|
if (session.busy && !force) {
|
|
996
1115
|
throw new ChatError('chat-busy', `Chat ${chatId} is still working on the previous message`);
|
|
997
1116
|
}
|
|
1117
|
+
|
|
998
1118
|
await this.hooks.clearing?.(chatId);
|
|
999
1119
|
const ended = session.clear(force);
|
|
1000
1120
|
// Before the record is written, so what it takes along from the record is gone from the write as well.
|
|
1001
1121
|
const cleared = this.hooks.cleared?.(chatId);
|
|
1002
1122
|
// A debounced write still waiting holds the old thread and must not land after the empty one.
|
|
1003
1123
|
this.cancelWaiting(chatId);
|
|
1124
|
+
|
|
1004
1125
|
// Folded right away: every line before the reset describes a thread that is gone.
|
|
1005
1126
|
await Promise.all([
|
|
1006
1127
|
this.persistNow(chatId, true),
|
|
@@ -1015,6 +1136,7 @@ export class ChatCore {
|
|
|
1015
1136
|
cancel(chatId: string, subagents = false): void {
|
|
1016
1137
|
const session = this.require(chatId);
|
|
1017
1138
|
session.cancel();
|
|
1139
|
+
|
|
1018
1140
|
if (subagents) {
|
|
1019
1141
|
session.markSubagentsStopped();
|
|
1020
1142
|
}
|
|
@@ -1047,6 +1169,7 @@ export class ChatCore {
|
|
|
1047
1169
|
holdForPerson(chatId: string, key: string): boolean {
|
|
1048
1170
|
const session = this.chats.get(chatId);
|
|
1049
1171
|
session?.holdForPerson(key);
|
|
1172
|
+
|
|
1050
1173
|
return session !== undefined;
|
|
1051
1174
|
}
|
|
1052
1175
|
|
|
@@ -1062,11 +1185,14 @@ export class ChatCore {
|
|
|
1062
1185
|
const session = this.require(chatId);
|
|
1063
1186
|
this.refuseUnlessRelocatable(session);
|
|
1064
1187
|
const running = this.relocateNow(session, cwd, during);
|
|
1188
|
+
|
|
1065
1189
|
const tracked = running.then(
|
|
1066
1190
|
() => undefined,
|
|
1067
1191
|
() => undefined
|
|
1068
1192
|
);
|
|
1193
|
+
|
|
1069
1194
|
this.relocations.set(chatId, tracked);
|
|
1195
|
+
|
|
1070
1196
|
try {
|
|
1071
1197
|
return await running;
|
|
1072
1198
|
} finally {
|
|
@@ -1084,12 +1210,15 @@ export class ChatCore {
|
|
|
1084
1210
|
private async relocateNow(session: ChatSession, cwd: string, during: (() => Promise<void>) | undefined): Promise<ChatInfo> {
|
|
1085
1211
|
const chatId = session.id;
|
|
1086
1212
|
await this.hooks.admit?.(chatId, cwd);
|
|
1213
|
+
|
|
1087
1214
|
// A message sent while the folder was asked about may have opened a turn.
|
|
1088
1215
|
if (!session.relocatable) {
|
|
1089
1216
|
throw this.busyRelocating(chatId);
|
|
1090
1217
|
}
|
|
1218
|
+
|
|
1091
1219
|
const previous = session.info.cwd;
|
|
1092
1220
|
session.beginRelocate();
|
|
1221
|
+
|
|
1093
1222
|
try {
|
|
1094
1223
|
await session.dropProcess();
|
|
1095
1224
|
await during?.();
|
|
@@ -1097,11 +1226,14 @@ export class ChatCore {
|
|
|
1097
1226
|
await this.persistNow(chatId, true);
|
|
1098
1227
|
} catch (e) {
|
|
1099
1228
|
session.setCwd(previous);
|
|
1229
|
+
|
|
1100
1230
|
throw e;
|
|
1101
1231
|
} finally {
|
|
1102
1232
|
session.endRelocate();
|
|
1103
1233
|
}
|
|
1234
|
+
|
|
1104
1235
|
this.skillIndex.forget(session.info.provider, previous);
|
|
1236
|
+
|
|
1105
1237
|
return session.info;
|
|
1106
1238
|
}
|
|
1107
1239
|
|
|
@@ -1125,9 +1257,11 @@ export class ChatCore {
|
|
|
1125
1257
|
async kill(chatId: string): Promise<void> {
|
|
1126
1258
|
await this.loaded(chatId);
|
|
1127
1259
|
const moving = this.relocations.get(chatId);
|
|
1260
|
+
|
|
1128
1261
|
if (moving) {
|
|
1129
1262
|
await moving;
|
|
1130
1263
|
}
|
|
1264
|
+
|
|
1131
1265
|
const session = this.require(chatId);
|
|
1132
1266
|
// A write already out would put the record back after the delete.
|
|
1133
1267
|
const writing = this.writes.get(chatId);
|
|
@@ -1151,6 +1285,7 @@ export class ChatCore {
|
|
|
1151
1285
|
this.hooks.forgotten?.(chatId);
|
|
1152
1286
|
await writing;
|
|
1153
1287
|
await ended;
|
|
1288
|
+
|
|
1154
1289
|
await Promise.all([
|
|
1155
1290
|
this.store?.delete(chatId),
|
|
1156
1291
|
this.attachments.removeAll(chatId),
|
|
@@ -1172,12 +1307,15 @@ export class ChatCore {
|
|
|
1172
1307
|
/* The chats with a CLI process, for a process monitor. */
|
|
1173
1308
|
processTargets(): { id: string; pid: number; provider: AgentKind; status: ChatInfo['status']; updatedAt: number }[] {
|
|
1174
1309
|
const targets = [];
|
|
1310
|
+
|
|
1175
1311
|
for (const [id, session] of this.chats) {
|
|
1176
1312
|
const pid = session.pid;
|
|
1313
|
+
|
|
1177
1314
|
if (pid !== null) {
|
|
1178
1315
|
targets.push({ id, pid, provider: session.info.provider, status: session.info.status, updatedAt: this.activity.get(id) ?? 0 });
|
|
1179
1316
|
}
|
|
1180
1317
|
}
|
|
1318
|
+
|
|
1181
1319
|
return targets;
|
|
1182
1320
|
}
|
|
1183
1321
|
|
|
@@ -1196,12 +1334,15 @@ export class ChatCore {
|
|
|
1196
1334
|
*/
|
|
1197
1335
|
async resumeRun(chatId: string, turnId: string, attempt: number): Promise<void> {
|
|
1198
1336
|
await this.loaded(chatId);
|
|
1337
|
+
|
|
1199
1338
|
if (!this.chats.has(chatId)) {
|
|
1200
1339
|
if (!(await this.store?.has(chatId))) {
|
|
1201
1340
|
return;
|
|
1202
1341
|
}
|
|
1342
|
+
|
|
1203
1343
|
await this.create({ chatId });
|
|
1204
1344
|
}
|
|
1345
|
+
|
|
1205
1346
|
await this.chats.get(chatId)?.resume(turnId, attempt, (await this.hooks.resumeWords?.(chatId)) ?? DEFAULT_RESUME_WORDS);
|
|
1206
1347
|
}
|
|
1207
1348
|
|
|
@@ -1211,22 +1352,30 @@ export class ChatCore {
|
|
|
1211
1352
|
*/
|
|
1212
1353
|
async takeUpAfterLimit(chatId: string, turnId: string): Promise<OutboxOutcome> {
|
|
1213
1354
|
await this.loaded(chatId);
|
|
1355
|
+
|
|
1214
1356
|
if (!this.chats.has(chatId)) {
|
|
1215
1357
|
if (!(await this.store?.has(chatId))) {
|
|
1216
1358
|
return;
|
|
1217
1359
|
}
|
|
1360
|
+
|
|
1218
1361
|
await this.create({ chatId });
|
|
1219
1362
|
}
|
|
1363
|
+
|
|
1220
1364
|
const session = this.chats.get(chatId);
|
|
1365
|
+
|
|
1221
1366
|
if (!session) {
|
|
1222
1367
|
return;
|
|
1223
1368
|
}
|
|
1369
|
+
|
|
1224
1370
|
const turn = session.thread.get(turnId);
|
|
1225
1371
|
const ended = this.hooks.endedAt?.(chatId) ?? null;
|
|
1372
|
+
|
|
1226
1373
|
if (turn !== undefined && ended !== null && ended >= turn.createdAt) {
|
|
1227
1374
|
session.lapseResume();
|
|
1375
|
+
|
|
1228
1376
|
return;
|
|
1229
1377
|
}
|
|
1378
|
+
|
|
1230
1379
|
return session.takeUpAfterLimit(turnId) === 'wait' ? 'wait' : undefined;
|
|
1231
1380
|
}
|
|
1232
1381
|
|
|
@@ -1251,16 +1400,20 @@ export class ChatCore {
|
|
|
1251
1400
|
if (!this.store) {
|
|
1252
1401
|
return;
|
|
1253
1402
|
}
|
|
1403
|
+
|
|
1254
1404
|
for (const chatId of await this.store.list()) {
|
|
1255
1405
|
if (this.chats.has(chatId) || this.creating.has(chatId)) {
|
|
1256
1406
|
continue;
|
|
1257
1407
|
}
|
|
1408
|
+
|
|
1258
1409
|
// Parsing the whole thread of every settled chat was most of the cost of a start.
|
|
1259
1410
|
if (await this.store.settled(chatId).catch(() => false)) {
|
|
1260
1411
|
continue;
|
|
1261
1412
|
}
|
|
1413
|
+
|
|
1262
1414
|
const record = await this.store.read(chatId).catch(() => null);
|
|
1263
1415
|
const unowed = record?.info.resumeAt !== undefined && this.limitResume?.owed(chatId) === false;
|
|
1416
|
+
|
|
1264
1417
|
if (record !== null && (interruptedTurn(record) !== null || unowed)) {
|
|
1265
1418
|
await this.create({ chatId }).catch((e: unknown) => console.error(`Loading chat ${chatId} after a restart failed:`, errorText(e)));
|
|
1266
1419
|
}
|
|
@@ -1275,13 +1428,16 @@ export class ChatCore {
|
|
|
1275
1428
|
for (const session of this.chats.values()) {
|
|
1276
1429
|
session.freeze();
|
|
1277
1430
|
}
|
|
1431
|
+
|
|
1278
1432
|
for (const chatId of this.chats.keys()) {
|
|
1279
1433
|
this.cancelWaiting(chatId);
|
|
1280
1434
|
}
|
|
1435
|
+
|
|
1281
1436
|
// A thread that cannot be written still ends its CLI, whose process group would otherwise outlive the host.
|
|
1282
1437
|
const written = await Promise.allSettled([...this.chats.keys()].map((chatId) => this.persistNow(chatId, true)));
|
|
1283
1438
|
await Promise.allSettled([...this.chats.values()].map((session) => session.dispose()));
|
|
1284
1439
|
const failed = written.find((result) => result.status === 'rejected');
|
|
1440
|
+
|
|
1285
1441
|
if (failed) {
|
|
1286
1442
|
throw failed.reason;
|
|
1287
1443
|
}
|
|
@@ -1295,14 +1451,17 @@ export class ChatCore {
|
|
|
1295
1451
|
if (!this.store) {
|
|
1296
1452
|
return;
|
|
1297
1453
|
}
|
|
1454
|
+
|
|
1298
1455
|
for (const session of this.chats.values()) {
|
|
1299
1456
|
session.freeze();
|
|
1300
1457
|
}
|
|
1458
|
+
|
|
1301
1459
|
for (const [chatId, session] of this.chats) {
|
|
1302
1460
|
this.cancelWaiting(chatId);
|
|
1303
1461
|
this.coalescers.get(chatId)?.flush();
|
|
1304
1462
|
const { info, items } = session.thread.snapshot();
|
|
1305
1463
|
const log = this.logs.get(chatId);
|
|
1464
|
+
|
|
1306
1465
|
// Not folded here: an older write still in flight may land after this one, and the log is what covers for it.
|
|
1307
1466
|
try {
|
|
1308
1467
|
this.store.writeSync(
|
|
@@ -1332,9 +1491,11 @@ export class ChatCore {
|
|
|
1332
1491
|
|
|
1333
1492
|
protected require(chatId: string): ChatSession {
|
|
1334
1493
|
const session = this.chats.get(chatId);
|
|
1494
|
+
|
|
1335
1495
|
if (!session) {
|
|
1336
1496
|
throw new ChatError('chat-not-found', `No chat ${chatId}`);
|
|
1337
1497
|
}
|
|
1498
|
+
|
|
1338
1499
|
return session;
|
|
1339
1500
|
}
|
|
1340
1501
|
|
|
@@ -1347,15 +1508,19 @@ export class ChatCore {
|
|
|
1347
1508
|
private persistSoon(chatId: string): void {
|
|
1348
1509
|
if ((this.sizes.get(chatId) ?? 0) < DEBOUNCE_ABOVE_BYTES) {
|
|
1349
1510
|
this.persist(chatId);
|
|
1511
|
+
|
|
1350
1512
|
return;
|
|
1351
1513
|
}
|
|
1514
|
+
|
|
1352
1515
|
if (this.waiting.has(chatId)) {
|
|
1353
1516
|
return;
|
|
1354
1517
|
}
|
|
1518
|
+
|
|
1355
1519
|
const timer = setTimeout(() => {
|
|
1356
1520
|
this.waiting.delete(chatId);
|
|
1357
1521
|
this.persist(chatId);
|
|
1358
1522
|
}, DEBOUNCE_MS);
|
|
1523
|
+
|
|
1359
1524
|
// A pending write must never be the reason the process stays up.
|
|
1360
1525
|
timer.unref?.();
|
|
1361
1526
|
this.waiting.set(chatId, timer);
|
|
@@ -1363,6 +1528,7 @@ export class ChatCore {
|
|
|
1363
1528
|
|
|
1364
1529
|
private cancelWaiting(chatId: string): void {
|
|
1365
1530
|
const timer = this.waiting.get(chatId);
|
|
1531
|
+
|
|
1366
1532
|
if (timer) {
|
|
1367
1533
|
clearTimeout(timer);
|
|
1368
1534
|
this.waiting.delete(chatId);
|
|
@@ -1377,33 +1543,40 @@ export class ChatCore {
|
|
|
1377
1543
|
const next = (this.writes.get(chatId) ?? Promise.resolve()).then(async () => {
|
|
1378
1544
|
const session = this.chats.get(chatId);
|
|
1379
1545
|
const log = this.logs.get(chatId);
|
|
1546
|
+
|
|
1380
1547
|
if (!session || !this.store || !log) {
|
|
1381
1548
|
return;
|
|
1382
1549
|
}
|
|
1550
|
+
|
|
1383
1551
|
// A delta held back is already in the thread, so it gets its seq before the snapshot says where it ends.
|
|
1384
1552
|
this.coalescers.get(chatId)?.flush();
|
|
1385
1553
|
const extras = this.hooks.recordExtras?.(chatId) ?? {};
|
|
1386
1554
|
const uiAccess = session.uiAccessRecord;
|
|
1387
1555
|
const unlogged = JSON.stringify([session.preambles, extras, uiAccess]);
|
|
1388
1556
|
const folds = fold || log.size > COMPACT_ABOVE_BYTES || !log.onDisk;
|
|
1557
|
+
|
|
1389
1558
|
// The log already holds every change to the thread, so the whole record is rewritten only to fold
|
|
1390
1559
|
// the log, on a chat's first write, or for what no event carries.
|
|
1391
1560
|
if (!folds && this.sizes.has(chatId) && this.unlogged.get(chatId) === unlogged) {
|
|
1392
1561
|
return;
|
|
1393
1562
|
}
|
|
1563
|
+
|
|
1394
1564
|
const { info, items } = session.thread.snapshot();
|
|
1395
1565
|
const at = { seq: log.seq, resetSeq: log.resetSeq };
|
|
1396
1566
|
this.sizes.set(chatId, await this.store.write(chatId, info, items, at, session.preambles, extras, uiAccess));
|
|
1397
1567
|
this.unlogged.set(chatId, unlogged);
|
|
1568
|
+
|
|
1398
1569
|
// A chat killed while the write was out has no log left to fold.
|
|
1399
1570
|
if (this.logs.get(chatId) === log && folds) {
|
|
1400
1571
|
log.compact(at.seq);
|
|
1401
1572
|
}
|
|
1402
1573
|
});
|
|
1574
|
+
|
|
1403
1575
|
this.writes.set(
|
|
1404
1576
|
chatId,
|
|
1405
1577
|
next.catch(() => undefined)
|
|
1406
1578
|
);
|
|
1579
|
+
|
|
1407
1580
|
return next;
|
|
1408
1581
|
}
|
|
1409
1582
|
|
|
@@ -1411,17 +1584,22 @@ export class ChatCore {
|
|
|
1411
1584
|
if (event.type === 'delta' && event.ui !== undefined) {
|
|
1412
1585
|
// Previews follow all pending text but have no log position or observer side effects.
|
|
1413
1586
|
this.coalescers.get(chatId)?.flush();
|
|
1587
|
+
|
|
1414
1588
|
for (const clientId of this.attached.get(chatId) ?? []) {
|
|
1415
1589
|
this.sinks.to(clientId, { event: 'chat.event', payload: { chatId, event } });
|
|
1416
1590
|
}
|
|
1591
|
+
|
|
1417
1592
|
return;
|
|
1418
1593
|
}
|
|
1594
|
+
|
|
1419
1595
|
this.activity.set(chatId, Date.now());
|
|
1420
1596
|
let coalescer = this.coalescers.get(chatId);
|
|
1597
|
+
|
|
1421
1598
|
if (!coalescer) {
|
|
1422
1599
|
coalescer = new DeltaCoalescer((coalesced) => this.broadcast(chatId, coalesced));
|
|
1423
1600
|
this.coalescers.set(chatId, coalescer);
|
|
1424
1601
|
}
|
|
1602
|
+
|
|
1425
1603
|
coalescer.push(event);
|
|
1426
1604
|
}
|
|
1427
1605
|
|
|
@@ -1429,18 +1607,23 @@ export class ChatCore {
|
|
|
1429
1607
|
private broadcast(chatId: string, event: ChatEvent): void {
|
|
1430
1608
|
const seq = this.logs.get(chatId)?.append(event, Date.now());
|
|
1431
1609
|
const payload = seq === undefined ? { chatId, event } : { chatId, event, seq };
|
|
1610
|
+
|
|
1432
1611
|
for (const sink of this.observers) {
|
|
1433
1612
|
sink({ event: 'chat.event', payload });
|
|
1434
1613
|
}
|
|
1614
|
+
|
|
1435
1615
|
this.hooks.broadcasted?.(chatId, event);
|
|
1616
|
+
|
|
1436
1617
|
for (const clientId of this.attached.get(chatId) ?? []) {
|
|
1437
1618
|
this.sinks.to(clientId, { event: 'chat.event', payload });
|
|
1438
1619
|
}
|
|
1620
|
+
|
|
1439
1621
|
// After the thread, so a client reading it has already had this info and the status is a no-op there.
|
|
1440
1622
|
if (event.type === 'info' || event.type === 'reset') {
|
|
1441
1623
|
this.announceStatus(chatId, event.info);
|
|
1442
1624
|
} else if (event.type === 'item' && (event.item.kind === 'approval' || event.item.kind === 'question')) {
|
|
1443
1625
|
const session = this.chats.get(chatId);
|
|
1626
|
+
|
|
1444
1627
|
if (session) {
|
|
1445
1628
|
this.announceStatus(chatId, session.info);
|
|
1446
1629
|
}
|
|
@@ -1465,9 +1648,11 @@ export class ChatCore {
|
|
|
1465
1648
|
info.requests?.map((request) => request.requestId) ?? [],
|
|
1466
1649
|
info.goal === undefined ? null : [info.goal.status, info.goal.objective]
|
|
1467
1650
|
]);
|
|
1651
|
+
|
|
1468
1652
|
if (this.announced.get(chatId) === said) {
|
|
1469
1653
|
return;
|
|
1470
1654
|
}
|
|
1655
|
+
|
|
1471
1656
|
this.announced.set(chatId, said);
|
|
1472
1657
|
this.sinks.emit({ event: 'chat.status', payload: { chatId, info } });
|
|
1473
1658
|
}
|
|
@@ -1481,42 +1666,57 @@ export class ChatCore {
|
|
|
1481
1666
|
if (!this.store) {
|
|
1482
1667
|
return new ChatLog(null);
|
|
1483
1668
|
}
|
|
1669
|
+
|
|
1484
1670
|
const path = this.store.logPath(chatId);
|
|
1671
|
+
|
|
1485
1672
|
if (stored) {
|
|
1486
1673
|
return new ChatLog(path, stored);
|
|
1487
1674
|
}
|
|
1675
|
+
|
|
1488
1676
|
const left = await this.store.discardLog(chatId);
|
|
1677
|
+
|
|
1489
1678
|
return new ChatLog(path, { seq: left, resetSeq: left === 0 ? 0 : left + 1, lines: [] });
|
|
1490
1679
|
}
|
|
1491
1680
|
|
|
1492
1681
|
/* Every reason says why in the words of the note the turn ends with, so a person can tell a skip from a failure. */
|
|
1493
1682
|
private async owedResume(chatId: string, stored: ChatRecord): Promise<ResumeDecision> {
|
|
1494
1683
|
const turn = interruptedTurn(stored);
|
|
1684
|
+
|
|
1495
1685
|
const skip = (reason: string): ResumeDecision => ({ resumeTurnId: null, reason });
|
|
1686
|
+
|
|
1496
1687
|
if (turn === null) {
|
|
1497
1688
|
return { resumeTurnId: null, reason: null };
|
|
1498
1689
|
}
|
|
1690
|
+
|
|
1499
1691
|
if (turn.deliveryPending) {
|
|
1500
1692
|
return skip('the wake prompt was not acknowledged; its result waits for your next message');
|
|
1501
1693
|
}
|
|
1694
|
+
|
|
1502
1695
|
if (!this.onInterruptedRun) {
|
|
1503
1696
|
return skip('this machine does not resume turns');
|
|
1504
1697
|
}
|
|
1698
|
+
|
|
1505
1699
|
if (stored.info.agentSessionId === null) {
|
|
1506
1700
|
return skip('the agent had not started a session to resume yet');
|
|
1507
1701
|
}
|
|
1702
|
+
|
|
1508
1703
|
const ended = this.hooks.endedAt?.(chatId) ?? null;
|
|
1704
|
+
|
|
1509
1705
|
if (ended !== null && ended >= turn.createdAt) {
|
|
1510
1706
|
return skip('the agent that opened it was stopped');
|
|
1511
1707
|
}
|
|
1708
|
+
|
|
1512
1709
|
if ((turn.attempt ?? 1) >= MAX_ATTEMPTS && !turn.resumePending) {
|
|
1513
1710
|
return skip('it was already resumed after an earlier restart');
|
|
1514
1711
|
}
|
|
1712
|
+
|
|
1515
1713
|
try {
|
|
1516
1714
|
const owed = await this.onInterruptedRun({ chatId, turnId: turn.id, attempt: (turn.attempt ?? 1) + (turn.resumePending ? 0 : 1) });
|
|
1715
|
+
|
|
1517
1716
|
return owed ? { resumeTurnId: turn.id, reason: null } : skip(this.hooks.unownedReason?.(chatId) ?? 'nothing holds this chat any more');
|
|
1518
1717
|
} catch (e) {
|
|
1519
1718
|
console.error(`Owing a resume for chat ${chatId} failed:`, errorText(e));
|
|
1719
|
+
|
|
1520
1720
|
return skip(errorText(e));
|
|
1521
1721
|
}
|
|
1522
1722
|
}
|
|
@@ -1537,5 +1737,6 @@ function limitHooks(hooks: NonNullable<ChatCoreOptions['limitResume']>, chatId:
|
|
|
1537
1737
|
function interruptedTurn(record: { info: ChatInfo; items: ChatItem[] }): Extract<ChatItem, { kind: 'turn' }> | null {
|
|
1538
1738
|
const turnId = record.info.activeTurnId;
|
|
1539
1739
|
const turn = turnId === null ? undefined : record.items.find((item) => item.id === turnId);
|
|
1740
|
+
|
|
1540
1741
|
return turn?.kind === 'turn' && turn.state === 'running' ? turn : null;
|
|
1541
1742
|
}
|