@adecore/agents 0.38.0 → 0.40.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/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.d.ts +1 -0
- package/dist/chat/chat-core.js +9 -2
- package/dist/chat/chat-process.d.ts +1 -0
- package/dist/chat/chat-process.js +1 -1
- package/dist/chat/chat-session.d.ts +2 -0
- package/dist/chat/chat-session.js +9 -10
- 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.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/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 +218 -7
- package/src/chat/chat-log.ts +19 -0
- package/src/chat/chat-process.ts +52 -1
- package/src/chat/chat-session.ts +414 -13
- 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-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/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-session.ts
CHANGED
|
@@ -34,7 +34,7 @@ import { readableAssistantText } from './readable-text.ts';
|
|
|
34
34
|
import type { SubagentSettlement } from './subagent-settlement.ts';
|
|
35
35
|
import { ChatThread } from './thread.ts';
|
|
36
36
|
import { UiReplies, type UiReplyHost } from './ui-replies.ts';
|
|
37
|
-
import { errorText } from '../error-text.ts';
|
|
37
|
+
import { asError, errorText } from '../error-text.ts';
|
|
38
38
|
import { systemClock, type OutboxClock } from '../outbox/outbox-worker.ts';
|
|
39
39
|
import { cleanTitle } from '../title-file.ts';
|
|
40
40
|
|
|
@@ -56,13 +56,13 @@ export interface PromptNotes {
|
|
|
56
56
|
reset(): void;
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
/* Chats a person attached to a message: the ones the host knows, and what the agent is told about them. */
|
|
60
59
|
/* What a message pointed at: other chats with `@`, and views and nodes with `#`, by id. */
|
|
61
60
|
export interface ChatAttached {
|
|
62
61
|
chats: string[];
|
|
63
62
|
references: string[];
|
|
64
63
|
}
|
|
65
64
|
|
|
65
|
+
/* Chats a person attached to a message: the ones the host knows, and what the agent is told about them. */
|
|
66
66
|
export type ChatReferences = (attached: {
|
|
67
67
|
readonly chats?: readonly string[];
|
|
68
68
|
readonly references?: readonly string[];
|
|
@@ -77,7 +77,6 @@ export interface ChatSessionOptions {
|
|
|
77
77
|
// The access a UI host captured per reply, from the record; never on the wire.
|
|
78
78
|
uiAccess?: Readonly<Record<string, unknown>>;
|
|
79
79
|
provider: ChatProvider;
|
|
80
|
-
// The executable and leading arguments; a test points this at a fake CLI.
|
|
81
80
|
command: string[];
|
|
82
81
|
/* The environment of the CLI under the chat's account, asked at every start; throws for an account that cannot start. */
|
|
83
82
|
env(account: string | undefined): Record<string, string>;
|
|
@@ -163,8 +162,10 @@ function newId(prefix: string): string {
|
|
|
163
162
|
* the ones before it, as long as it handed out fewer than one per millisecond it ran.
|
|
164
163
|
*/
|
|
165
164
|
let lastGeneration = Date.now();
|
|
165
|
+
|
|
166
166
|
function nextGeneration(): number {
|
|
167
167
|
lastGeneration = Math.max(lastGeneration + 1, Date.now());
|
|
168
|
+
|
|
168
169
|
return lastGeneration;
|
|
169
170
|
}
|
|
170
171
|
|
|
@@ -235,6 +236,7 @@ export class ChatSession {
|
|
|
235
236
|
this.capturedUiAccess = new Map(Object.entries(options.uiAccess ?? {}));
|
|
236
237
|
this.thread = new ChatThread(options.info, options.items);
|
|
237
238
|
this.projector = new ThreadProjector(this.thread, { providerName: options.provider.name });
|
|
239
|
+
|
|
238
240
|
this.uiReplies = new UiReplies({
|
|
239
241
|
thread: this.thread,
|
|
240
242
|
fenceLanguage: options.uiFenceLanguage ?? UI_FENCE_LANGUAGE,
|
|
@@ -286,6 +288,7 @@ export class ChatSession {
|
|
|
286
288
|
get busy(): boolean {
|
|
287
289
|
const turnId = this.thread.info.activeTurnId;
|
|
288
290
|
const turn = turnId ? this.thread.get(turnId) : undefined;
|
|
291
|
+
|
|
289
292
|
// A turn the CLI opened toward a goal is the goal's own: a message waits for it like for any turn of the person's.
|
|
290
293
|
return turnId !== null && (turn?.kind !== 'turn' || turn.origin !== 'agent' || this.goalWorks());
|
|
291
294
|
}
|
|
@@ -302,20 +305,27 @@ export class ChatSession {
|
|
|
302
305
|
*/
|
|
303
306
|
async goal(action: ChatGoalAction): Promise<void> {
|
|
304
307
|
const supported = this.options.provider.capabilities.goals;
|
|
308
|
+
|
|
305
309
|
if (supported === undefined || (supported === 'command' && action !== 'clear')) {
|
|
306
310
|
throw new ChatError('chat-unsupported', `${this.options.provider.name} cannot ${action} a goal`);
|
|
307
311
|
}
|
|
312
|
+
|
|
308
313
|
if (this.thread.info.goal === undefined) {
|
|
309
314
|
throw new ChatError('goal-not-found', 'This chat has no goal');
|
|
310
315
|
}
|
|
316
|
+
|
|
311
317
|
const backend = this.backend;
|
|
318
|
+
|
|
312
319
|
if (supported === 'native' && backend?.goal && backend.running && (action !== 'resume' || this.busy)) {
|
|
313
320
|
await backend.goal(action);
|
|
321
|
+
|
|
314
322
|
return;
|
|
315
323
|
}
|
|
324
|
+
|
|
316
325
|
if (this.busy) {
|
|
317
326
|
throw new ChatError('chat-busy', 'Stop the turn that works toward the goal first');
|
|
318
327
|
}
|
|
328
|
+
|
|
319
329
|
this.send(`/goal ${action}`);
|
|
320
330
|
}
|
|
321
331
|
|
|
@@ -326,22 +336,29 @@ export class ChatSession {
|
|
|
326
336
|
this.options.persist();
|
|
327
337
|
this.resumeSettingChanged();
|
|
328
338
|
}
|
|
339
|
+
|
|
329
340
|
const catalog = this.options.provider.catalog;
|
|
330
341
|
const selection = patch.selection ? catalog.normalize(patch.selection) : this.thread.info.selection;
|
|
342
|
+
|
|
331
343
|
const next: Partial<ChatInfo> = {
|
|
332
344
|
selection,
|
|
333
345
|
runtimeMode: patch.runtimeMode ?? this.thread.info.runtimeMode
|
|
334
346
|
};
|
|
347
|
+
|
|
335
348
|
const changed = JSON.stringify(next.selection) !== JSON.stringify(this.thread.info.selection) || next.runtimeMode !== this.thread.info.runtimeMode;
|
|
349
|
+
|
|
336
350
|
if (!changed) {
|
|
337
351
|
return this.thread.info;
|
|
338
352
|
}
|
|
353
|
+
|
|
339
354
|
if (patch.selection) {
|
|
340
355
|
next.usage = { ...this.thread.info.usage, contextWindow: catalog.contextWindowFor(selection) };
|
|
341
356
|
}
|
|
357
|
+
|
|
342
358
|
this.emit([this.thread.patchInfo(next)]);
|
|
343
359
|
this.options.persist();
|
|
344
360
|
this.drainQueue();
|
|
361
|
+
|
|
345
362
|
return this.thread.info;
|
|
346
363
|
}
|
|
347
364
|
|
|
@@ -350,9 +367,11 @@ export class ChatSession {
|
|
|
350
367
|
if (account === this.thread.info.account) {
|
|
351
368
|
return this.thread.info;
|
|
352
369
|
}
|
|
370
|
+
|
|
353
371
|
this.emit([this.thread.patchInfo({ account })]);
|
|
354
372
|
this.options.persist();
|
|
355
373
|
this.drainQueue();
|
|
374
|
+
|
|
356
375
|
return this.thread.info;
|
|
357
376
|
}
|
|
358
377
|
|
|
@@ -361,8 +380,10 @@ export class ChatSession {
|
|
|
361
380
|
if (cwd === this.thread.info.cwd) {
|
|
362
381
|
return this.thread.info;
|
|
363
382
|
}
|
|
383
|
+
|
|
364
384
|
this.emit([this.thread.patchInfo({ cwd })]);
|
|
365
385
|
this.options.persist();
|
|
386
|
+
|
|
366
387
|
return this.thread.info;
|
|
367
388
|
}
|
|
368
389
|
|
|
@@ -396,17 +417,17 @@ export class ChatSession {
|
|
|
396
417
|
this.emit([this.thread.patchInfo({})]);
|
|
397
418
|
}
|
|
398
419
|
|
|
399
|
-
/*
|
|
400
|
-
* Internal follow-ups wait for a new turn. User input goes through `sendInput`, which can steer native work.
|
|
401
|
-
*/
|
|
420
|
+
/* Internal follow-ups wait for a new turn. User input goes through `sendInput`, which can steer native work. */
|
|
402
421
|
send(text: string, sent: ChatSendExtras = {}): { queued: boolean; turnId: string } {
|
|
403
422
|
const extras = this.vetted(sent);
|
|
404
423
|
const turnId = newId('turn');
|
|
424
|
+
|
|
405
425
|
// Behind whatever still waits (a CLI that crashed leaves its queue), so everything goes out in the order it was sent.
|
|
406
426
|
if (this.busy || this.queue.length > 0 || this.replacementWaits() || this.relocating) {
|
|
407
427
|
if (this.busy && this.info.queuePaused) {
|
|
408
428
|
this.sendAfterStop = true;
|
|
409
429
|
}
|
|
430
|
+
|
|
410
431
|
const message: ChatQueuedMessage = {
|
|
411
432
|
id: newId('queued'),
|
|
412
433
|
turnId,
|
|
@@ -420,12 +441,16 @@ export class ChatSession {
|
|
|
420
441
|
...(extras.attachments?.length ? { attachments: extras.attachments } : {}),
|
|
421
442
|
...(extras.references?.length ? { references: extras.references } : {})
|
|
422
443
|
};
|
|
444
|
+
|
|
423
445
|
this.setQueue([...this.queue, message]);
|
|
424
446
|
this.lapseResume();
|
|
425
447
|
this.drainQueue(true);
|
|
448
|
+
|
|
426
449
|
return { queued: true, turnId };
|
|
427
450
|
}
|
|
451
|
+
|
|
428
452
|
this.dispatch(text, extras, turnId);
|
|
453
|
+
|
|
429
454
|
return { queued: false, turnId };
|
|
430
455
|
}
|
|
431
456
|
|
|
@@ -438,6 +463,7 @@ export class ChatSession {
|
|
|
438
463
|
if (this.frozen) {
|
|
439
464
|
return;
|
|
440
465
|
}
|
|
466
|
+
|
|
441
467
|
this.capturedUiAccess.set(itemId, access);
|
|
442
468
|
this.options.persistSoon();
|
|
443
469
|
}
|
|
@@ -449,9 +475,11 @@ export class ChatSession {
|
|
|
449
475
|
|
|
450
476
|
updateUiQueries(itemId: string, uiQueries: ChatUiQueryState): void {
|
|
451
477
|
const item = this.thread.get(itemId);
|
|
478
|
+
|
|
452
479
|
if (this.frozen || item?.kind !== 'assistant' || item.parentToolUseId) {
|
|
453
480
|
return;
|
|
454
481
|
}
|
|
482
|
+
|
|
455
483
|
this.thread.upsert({ ...item, uiQueries });
|
|
456
484
|
this.options.emit({ type: 'delta', itemId, text: '', uiQueries });
|
|
457
485
|
this.options.persistSoon();
|
|
@@ -459,35 +487,46 @@ export class ChatSession {
|
|
|
459
487
|
|
|
460
488
|
choose(payload: ChatUiChoicePayload, queries: Readonly<Record<string, unknown>> = {}): { queued: boolean; turnId: string } {
|
|
461
489
|
const item = this.thread.get(payload.itemId);
|
|
490
|
+
|
|
462
491
|
const block =
|
|
463
492
|
item?.kind === 'assistant' && !item.streaming && !item.parentToolUseId ? item.ui?.find((block) => block.id === payload.blockId) : undefined;
|
|
493
|
+
|
|
464
494
|
if (item?.kind !== 'assistant' || !block?.revision || block.revision !== payload.revision) {
|
|
465
495
|
throw new ChatError('stale-ui-block', 'This completed UI block is no longer available at the requested revision');
|
|
466
496
|
}
|
|
497
|
+
|
|
467
498
|
const sameBlock = (origin: ChatUiChoiceOrigin | undefined) =>
|
|
468
499
|
origin?.itemId === item.id && origin.blockId === block.id && origin.revision === block.revision;
|
|
500
|
+
|
|
469
501
|
let previous = item.uiAnswers?.[block.id];
|
|
502
|
+
|
|
470
503
|
if (previous?.revision !== block.revision) {
|
|
471
504
|
previous = undefined;
|
|
472
505
|
}
|
|
506
|
+
|
|
473
507
|
// The message is logged first: a crash before its answer marker must not send it a second time.
|
|
474
508
|
if (!previous) {
|
|
475
509
|
const sent = this.thread.list().find((entry) => entry.kind === 'user' && sameBlock(entry.uiChoice));
|
|
476
510
|
const queued = this.queue.find((entry) => sameBlock(entry.uiChoice));
|
|
477
511
|
const origin = sent?.kind === 'user' ? sent.uiChoice : queued?.uiChoice;
|
|
478
512
|
const turnId = sent?.turnId ?? queued?.turnId;
|
|
513
|
+
|
|
479
514
|
if (origin && turnId) {
|
|
480
515
|
previous = { ...origin, turnId, queued: !sent };
|
|
481
516
|
this.recordUiAnswer(previous);
|
|
482
517
|
}
|
|
483
518
|
}
|
|
519
|
+
|
|
484
520
|
if (previous) {
|
|
485
521
|
if (previous.choiceId !== payload.choiceId) {
|
|
486
522
|
throw new ChatError('ui-already-answered', 'This UI block was already answered');
|
|
487
523
|
}
|
|
524
|
+
|
|
488
525
|
return { queued: previous.queued, turnId: previous.turnId };
|
|
489
526
|
}
|
|
527
|
+
|
|
490
528
|
let selected;
|
|
529
|
+
|
|
491
530
|
try {
|
|
492
531
|
selected = resolveUiChoice(block, payload.choiceId, payload.values, queries);
|
|
493
532
|
} catch (error) {
|
|
@@ -496,6 +535,7 @@ export class ChatSession {
|
|
|
496
535
|
error instanceof Error ? error.message : 'This choice could not be read'
|
|
497
536
|
);
|
|
498
537
|
}
|
|
538
|
+
|
|
499
539
|
const origin: ChatUiChoiceOrigin = {
|
|
500
540
|
itemId: item.id,
|
|
501
541
|
blockId: block.id,
|
|
@@ -507,19 +547,24 @@ export class ChatSession {
|
|
|
507
547
|
values: selected.values,
|
|
508
548
|
at: Date.now()
|
|
509
549
|
};
|
|
550
|
+
|
|
510
551
|
const result = this.send(selected.context, { uiChoice: origin });
|
|
511
552
|
this.recordUiAnswer({ ...origin, ...result });
|
|
553
|
+
|
|
512
554
|
return result;
|
|
513
555
|
}
|
|
514
556
|
|
|
515
557
|
private recordUiAnswer(answer: ChatUiAnswer): void {
|
|
516
558
|
const item = this.thread.get(answer.itemId);
|
|
559
|
+
|
|
517
560
|
if (item?.kind !== 'assistant') {
|
|
518
561
|
return;
|
|
519
562
|
}
|
|
563
|
+
|
|
520
564
|
if (JSON.stringify(item.uiAnswers?.[answer.blockId]) === JSON.stringify(answer)) {
|
|
521
565
|
return;
|
|
522
566
|
}
|
|
567
|
+
|
|
523
568
|
this.options.emit(this.thread.upsert({ ...item, uiAnswers: { ...item.uiAnswers, [answer.blockId]: answer } }));
|
|
524
569
|
this.options.persist();
|
|
525
570
|
}
|
|
@@ -529,8 +574,10 @@ export class ChatSession {
|
|
|
529
574
|
if (extras.quoteSource === undefined || this.thread.get(extras.quoteSource.itemId)?.kind === 'assistant') {
|
|
530
575
|
return extras;
|
|
531
576
|
}
|
|
577
|
+
|
|
532
578
|
const plain = { ...extras };
|
|
533
579
|
delete plain.quoteSource;
|
|
580
|
+
|
|
534
581
|
return plain;
|
|
535
582
|
}
|
|
536
583
|
|
|
@@ -540,6 +587,7 @@ export class ChatSession {
|
|
|
540
587
|
if (text.startsWith('/') || !this.options.references) {
|
|
541
588
|
return { chats: [], references: [], note: null };
|
|
542
589
|
}
|
|
590
|
+
|
|
543
591
|
return this.options.references({ chats: extras.chats, references: extras.references });
|
|
544
592
|
}
|
|
545
593
|
|
|
@@ -551,6 +599,7 @@ export class ChatSession {
|
|
|
551
599
|
const extras = this.vetted(sent);
|
|
552
600
|
const backend = this.backend;
|
|
553
601
|
const turnId = this.info.activeTurnId;
|
|
602
|
+
|
|
554
603
|
if (
|
|
555
604
|
extras.delivery !== 'steer' ||
|
|
556
605
|
this.info.queuePaused ||
|
|
@@ -565,26 +614,34 @@ export class ChatSession {
|
|
|
565
614
|
) {
|
|
566
615
|
return this.send(text, extras);
|
|
567
616
|
}
|
|
617
|
+
|
|
568
618
|
const queued = this.send(text, extras);
|
|
569
619
|
const message = this.queue.find((entry) => entry.turnId === queued.turnId);
|
|
620
|
+
|
|
570
621
|
if (!message) {
|
|
571
622
|
return queued;
|
|
572
623
|
}
|
|
624
|
+
|
|
573
625
|
this.steering.add(message.id);
|
|
574
626
|
const steer = this.steer(message, text, extras, backend.steerTurn.bind(backend), turnId, queued);
|
|
575
627
|
// Claude takes a steer only between tool calls, so a long command would outlast the client's request.
|
|
576
628
|
const clock = this.options.clock ?? systemClock;
|
|
577
629
|
let timer: unknown = null;
|
|
630
|
+
|
|
578
631
|
const waited = new Promise<null>((resolve) => {
|
|
579
632
|
timer = clock.setTimeout(() => resolve(null), STEER_ANSWER_MS);
|
|
580
633
|
});
|
|
634
|
+
|
|
581
635
|
try {
|
|
582
636
|
const answer = await Promise.race([steer, waited]);
|
|
637
|
+
|
|
583
638
|
if (answer !== null) {
|
|
584
639
|
return answer;
|
|
585
640
|
}
|
|
641
|
+
|
|
586
642
|
// Still in the queue until it is taken; a later failure pauses the queue as it does now.
|
|
587
643
|
steer.catch(() => undefined);
|
|
644
|
+
|
|
588
645
|
return queued;
|
|
589
646
|
} finally {
|
|
590
647
|
clock.clearTimeout(timer);
|
|
@@ -601,6 +658,7 @@ export class ChatSession {
|
|
|
601
658
|
): Promise<{ queued: boolean; turnId: string }> {
|
|
602
659
|
try {
|
|
603
660
|
const attached = this.attached(text, extras);
|
|
661
|
+
|
|
604
662
|
const accepted = await steerTurn({
|
|
605
663
|
promptId: message.id,
|
|
606
664
|
text,
|
|
@@ -609,9 +667,11 @@ export class ChatSession {
|
|
|
609
667
|
mentions: extras.mentions ?? [],
|
|
610
668
|
skills: extras.skills ?? []
|
|
611
669
|
});
|
|
670
|
+
|
|
612
671
|
if (!accepted) {
|
|
613
672
|
return queued;
|
|
614
673
|
}
|
|
674
|
+
|
|
615
675
|
this.emit([
|
|
616
676
|
this.thread.upsert({
|
|
617
677
|
id: `user-${message.id}`,
|
|
@@ -628,12 +688,15 @@ export class ChatSession {
|
|
|
628
688
|
}),
|
|
629
689
|
this.thread.patchInfo({ queue: this.queue.filter((entry) => entry.id !== message.id) })
|
|
630
690
|
]);
|
|
691
|
+
|
|
631
692
|
this.options.persist();
|
|
693
|
+
|
|
632
694
|
return { queued: false, turnId };
|
|
633
695
|
} catch (error) {
|
|
634
696
|
// A lost acknowledgement does not prove rejection. Keep the message for an explicit retry.
|
|
635
697
|
this.pauseQueue(true);
|
|
636
698
|
this.options.persist();
|
|
699
|
+
|
|
637
700
|
throw error;
|
|
638
701
|
} finally {
|
|
639
702
|
this.steering.delete(message.id);
|
|
@@ -645,10 +708,13 @@ export class ChatSession {
|
|
|
645
708
|
unqueue(messageId: string): ChatQueuedMessage | null {
|
|
646
709
|
const queue = this.queue;
|
|
647
710
|
const message = queue.find((entry) => entry.id === messageId);
|
|
711
|
+
|
|
648
712
|
if (!message || this.steering.has(message.id)) {
|
|
649
713
|
return null;
|
|
650
714
|
}
|
|
715
|
+
|
|
651
716
|
this.setQueue(queue.filter((entry) => entry.id !== messageId));
|
|
717
|
+
|
|
652
718
|
return message;
|
|
653
719
|
}
|
|
654
720
|
|
|
@@ -656,16 +722,22 @@ export class ChatSession {
|
|
|
656
722
|
sendNow(messageId: string): boolean {
|
|
657
723
|
const queue = this.queue;
|
|
658
724
|
const message = queue.find((entry) => entry.id === messageId);
|
|
725
|
+
|
|
659
726
|
if (!message || this.steering.has(message.id)) {
|
|
660
727
|
return false;
|
|
661
728
|
}
|
|
729
|
+
|
|
662
730
|
this.setQueue([message, ...queue.filter((entry) => entry.id !== messageId)]);
|
|
731
|
+
|
|
663
732
|
if (this.busy) {
|
|
664
733
|
this.cancel(true);
|
|
734
|
+
|
|
665
735
|
return true;
|
|
666
736
|
}
|
|
737
|
+
|
|
667
738
|
this.lapseResume();
|
|
668
739
|
this.drainQueue(true);
|
|
740
|
+
|
|
669
741
|
return true;
|
|
670
742
|
}
|
|
671
743
|
|
|
@@ -683,6 +755,7 @@ export class ChatSession {
|
|
|
683
755
|
const [next, ...rest] = this.queue;
|
|
684
756
|
const limited = limitedTurn(this.thread.list());
|
|
685
757
|
const explicit = asked || this.sendAfterReplacement;
|
|
758
|
+
|
|
686
759
|
if (
|
|
687
760
|
!next ||
|
|
688
761
|
this.steering.size > 0 ||
|
|
@@ -692,9 +765,11 @@ export class ChatSession {
|
|
|
692
765
|
) {
|
|
693
766
|
return;
|
|
694
767
|
}
|
|
768
|
+
|
|
695
769
|
if (this.replacementWaits()) {
|
|
696
770
|
this.sendAfterReplacement = explicit;
|
|
697
771
|
const id = `settings-wait-${this.generation}`;
|
|
772
|
+
|
|
698
773
|
if (!this.thread.get(id)) {
|
|
699
774
|
this.emit([
|
|
700
775
|
this.thread.upsert({
|
|
@@ -706,13 +781,17 @@ export class ChatSession {
|
|
|
706
781
|
text: 'Your message is queued until the current work and open requests finish. The changes to this chat will apply then.'
|
|
707
782
|
})
|
|
708
783
|
]);
|
|
784
|
+
|
|
709
785
|
this.options.persist();
|
|
710
786
|
}
|
|
787
|
+
|
|
711
788
|
return;
|
|
712
789
|
}
|
|
790
|
+
|
|
713
791
|
this.sendAfterReplacement = false;
|
|
714
792
|
this.pauseQueue(false);
|
|
715
793
|
this.setQueue(rest);
|
|
794
|
+
|
|
716
795
|
this.dispatch(
|
|
717
796
|
next.text,
|
|
718
797
|
{
|
|
@@ -731,17 +810,22 @@ export class ChatSession {
|
|
|
731
810
|
private dispatch(text: string, extras: ChatSendExtras, requestedTurnId?: string): void {
|
|
732
811
|
this.pauseQueue(false);
|
|
733
812
|
this.settleAgentTurn();
|
|
813
|
+
|
|
734
814
|
// A goal that was met has said what it had to; the next message of the person leaves it behind.
|
|
735
815
|
if (this.thread.info.goal?.status === 'complete') {
|
|
736
816
|
this.emit([this.thread.patchInfo({ goal: undefined })]);
|
|
737
817
|
}
|
|
818
|
+
|
|
738
819
|
const { preamble, note } = this.contextNote(text);
|
|
739
820
|
const attached = this.attached(text, extras);
|
|
740
821
|
const turnId = this.openTurn(text, note, { ...extras, chats: attached.chats, references: attached.references }, requestedTurnId);
|
|
822
|
+
|
|
741
823
|
const choiceNote = extras.uiChoice
|
|
742
824
|
? `The person chose ${JSON.stringify(extras.uiChoice.label)} from your UI reply. Their message is the visible context of that choice.`
|
|
743
825
|
: null;
|
|
826
|
+
|
|
744
827
|
const said = [preamble, attached.note, choiceNote].filter((part): part is string => part !== null);
|
|
828
|
+
|
|
745
829
|
const input = {
|
|
746
830
|
text,
|
|
747
831
|
preamble: said.length === 0 ? null : said.join('\n\n'),
|
|
@@ -749,6 +833,7 @@ export class ChatSession {
|
|
|
749
833
|
mentions: extras.mentions ?? [],
|
|
750
834
|
skills: extras.skills ?? []
|
|
751
835
|
};
|
|
836
|
+
|
|
752
837
|
// The prompt waits for the checkpoint, so the tree is the folder as it was before the agent edited it.
|
|
753
838
|
this.turnReady = this.checkpoint(turnId);
|
|
754
839
|
this.submit(turnId, input);
|
|
@@ -757,16 +842,21 @@ export class ChatSession {
|
|
|
757
842
|
/* Asks the CLI to fold its context: a call of its own, or the slash command as a turn. */
|
|
758
843
|
compact(): void {
|
|
759
844
|
const compaction = this.options.provider.capabilities.compaction;
|
|
845
|
+
|
|
760
846
|
if (compaction === 'none') {
|
|
761
847
|
throw new ChatError('chat-unsupported', `${this.options.provider.name} cannot compact its context`);
|
|
762
848
|
}
|
|
849
|
+
|
|
763
850
|
if (compaction === 'prompt') {
|
|
764
851
|
this.send('/compact');
|
|
852
|
+
|
|
765
853
|
return;
|
|
766
854
|
}
|
|
855
|
+
|
|
767
856
|
if (this.replacementWaits()) {
|
|
768
857
|
throw new ChatError('chat-busy', 'The current work and open requests must finish before the changes to this chat can apply');
|
|
769
858
|
}
|
|
859
|
+
|
|
770
860
|
// A native compaction has no message of the person in front of it, only a turn to fold behind.
|
|
771
861
|
this.openTurn(null, null, {});
|
|
772
862
|
// Folding the context changes no file, so this turn needs no checkpoint.
|
|
@@ -780,12 +870,15 @@ export class ChatSession {
|
|
|
780
870
|
this.pauseQueue(true);
|
|
781
871
|
this.lapseResume();
|
|
782
872
|
const turnId = this.thread.info.activeTurnId;
|
|
873
|
+
|
|
783
874
|
// A turn still waiting to be resumed has no process to interrupt; stopping it is ending it here.
|
|
784
875
|
if (!this.backend && turnId !== null) {
|
|
785
876
|
this.clearStartWarning();
|
|
786
877
|
this.abandon(turnId, null);
|
|
878
|
+
|
|
787
879
|
return;
|
|
788
880
|
}
|
|
881
|
+
|
|
789
882
|
if (!this.backend || turnId === null) {
|
|
790
883
|
if (turnId === null && this.backend) {
|
|
791
884
|
for (const item of this.thread.pending()) {
|
|
@@ -793,10 +886,13 @@ export class ChatSession {
|
|
|
793
886
|
this.receive(this.generation, { type: 'request.withdrawn', requestId: item.requestId });
|
|
794
887
|
}
|
|
795
888
|
}
|
|
889
|
+
|
|
796
890
|
this.options.persist();
|
|
797
891
|
}
|
|
892
|
+
|
|
798
893
|
return;
|
|
799
894
|
}
|
|
895
|
+
|
|
800
896
|
if (this.pendingStart?.turnId === turnId) {
|
|
801
897
|
this.clearStartWarning();
|
|
802
898
|
const backend = this.backend;
|
|
@@ -805,8 +901,10 @@ export class ChatSession {
|
|
|
805
901
|
backend.stop();
|
|
806
902
|
this.emit([this.thread.patchInfo({ running: false })]);
|
|
807
903
|
this.receive(this.generation, { type: 'turn.done', state: 'aborted', costUsd: 0 });
|
|
904
|
+
|
|
808
905
|
return;
|
|
809
906
|
}
|
|
907
|
+
|
|
810
908
|
this.backend.interrupt();
|
|
811
909
|
}
|
|
812
910
|
|
|
@@ -815,32 +913,41 @@ export class ChatSession {
|
|
|
815
913
|
const workflow = this.thread
|
|
816
914
|
.list()
|
|
817
915
|
.some((item) => item.kind === 'tool' && item.name === 'Workflow' && item.state === 'running' && item.workflow?.taskId === taskId);
|
|
916
|
+
|
|
818
917
|
if (!workflow && !this.thread.info.background?.some((task) => task.id === taskId)) {
|
|
819
918
|
throw new ChatError('task-not-found', 'This chat runs no such task');
|
|
820
919
|
}
|
|
920
|
+
|
|
821
921
|
if (!this.backend?.stopTask) {
|
|
822
922
|
throw new ChatError('chat-unsupported', 'This CLI cannot stop a task on its own');
|
|
823
923
|
}
|
|
924
|
+
|
|
824
925
|
this.backend.stopTask(taskId);
|
|
825
926
|
}
|
|
826
927
|
|
|
827
928
|
/* Answers a pending approval; false when nothing waits under that id. */
|
|
828
929
|
approve(requestId: string, decision: 'allow' | 'allow-always' | 'deny', message?: string): boolean {
|
|
829
930
|
const item = this.thread.get(`approval-${requestId}`);
|
|
931
|
+
|
|
830
932
|
if (item?.kind !== 'approval' || item.decision !== 'pending' || this.backend?.respondApproval(requestId, decision, message) !== true) {
|
|
831
933
|
return false;
|
|
832
934
|
}
|
|
935
|
+
|
|
833
936
|
this.emit([this.thread.upsert({ ...item, decision }), this.thread.setStatus(this.thread.statusFor(this.thread.info.activeTurnId))]);
|
|
937
|
+
|
|
834
938
|
return true;
|
|
835
939
|
}
|
|
836
940
|
|
|
837
941
|
/* Answers a pending question; false when nothing waits under that id. */
|
|
838
942
|
answer(requestId: string, answers: Record<string, string>): boolean {
|
|
839
943
|
const item = this.thread.get(`question-${requestId}`);
|
|
944
|
+
|
|
840
945
|
if (item?.kind !== 'question' || item.state !== 'pending' || this.backend?.respondQuestion(requestId, answers) !== true) {
|
|
841
946
|
return false;
|
|
842
947
|
}
|
|
948
|
+
|
|
843
949
|
this.emit([this.thread.upsert({ ...item, answers, state: 'answered' }), this.thread.setStatus(this.thread.statusFor(this.thread.info.activeTurnId))]);
|
|
950
|
+
|
|
844
951
|
return true;
|
|
845
952
|
}
|
|
846
953
|
|
|
@@ -850,11 +957,14 @@ export class ChatSession {
|
|
|
850
957
|
*/
|
|
851
958
|
dismiss(itemId: string): boolean {
|
|
852
959
|
const item = this.thread.get(itemId);
|
|
960
|
+
|
|
853
961
|
if (item?.kind !== 'question' || item.state !== 'pending' || item.async !== true) {
|
|
854
962
|
return false;
|
|
855
963
|
}
|
|
964
|
+
|
|
856
965
|
this.backend?.dismissRequest?.(item.requestId);
|
|
857
966
|
this.emit([this.thread.upsert({ ...item, state: 'dismissed' }), this.thread.setStatus(this.thread.statusFor(this.thread.info.activeTurnId))]);
|
|
967
|
+
|
|
858
968
|
return true;
|
|
859
969
|
}
|
|
860
970
|
|
|
@@ -883,6 +993,7 @@ export class ChatSession {
|
|
|
883
993
|
if (this.busy && !force) {
|
|
884
994
|
throw new ChatError('chat-busy', `Chat ${this.id} is still working on the previous message`);
|
|
885
995
|
}
|
|
996
|
+
|
|
886
997
|
const ended = this.dispose();
|
|
887
998
|
this.generation = nextGeneration();
|
|
888
999
|
this.projector.reset();
|
|
@@ -900,6 +1011,7 @@ export class ChatSession {
|
|
|
900
1011
|
this.turnReady = Promise.resolve();
|
|
901
1012
|
this.lapseResume();
|
|
902
1013
|
const usage = this.thread.info.usage;
|
|
1014
|
+
|
|
903
1015
|
this.emit([
|
|
904
1016
|
this.thread.reset({
|
|
905
1017
|
agentSessionId: null,
|
|
@@ -926,6 +1038,7 @@ export class ChatSession {
|
|
|
926
1038
|
}
|
|
927
1039
|
})
|
|
928
1040
|
]);
|
|
1041
|
+
|
|
929
1042
|
return ended;
|
|
930
1043
|
}
|
|
931
1044
|
|
|
@@ -938,23 +1051,31 @@ export class ChatSession {
|
|
|
938
1051
|
const events: ChatEvent[] = [];
|
|
939
1052
|
const resumeTurnId = resume.resumeTurnId;
|
|
940
1053
|
const now = Date.now();
|
|
1054
|
+
|
|
941
1055
|
for (const item of this.thread.list()) {
|
|
942
1056
|
const settled = settleStoredItem(item, resumeTurnId);
|
|
1057
|
+
|
|
943
1058
|
if (settled === item) {
|
|
944
1059
|
continue;
|
|
945
1060
|
}
|
|
1061
|
+
|
|
946
1062
|
if (isBackgroundWork(item)) {
|
|
947
1063
|
this.recordLostBackground(item);
|
|
948
1064
|
}
|
|
1065
|
+
|
|
949
1066
|
events.push(this.thread.upsert(settled));
|
|
1067
|
+
|
|
950
1068
|
if (item.kind === 'turn' && settled.kind === 'turn' && settled.state === 'aborted') {
|
|
951
1069
|
const reason = item.id === this.thread.info.activeTurnId && resume.reason !== null ? resume.reason : 'it was not the turn the chat was running';
|
|
1070
|
+
|
|
952
1071
|
events.push(
|
|
953
1072
|
this.thread.upsert({ id: newId('note'), kind: 'note', createdAt: now, turnId: item.id, level: 'warning', text: notResumedNote(reason) })
|
|
954
1073
|
);
|
|
955
1074
|
}
|
|
956
1075
|
}
|
|
1076
|
+
|
|
957
1077
|
const info = this.thread.info;
|
|
1078
|
+
|
|
958
1079
|
const patch: Partial<ChatInfo> = {
|
|
959
1080
|
selection,
|
|
960
1081
|
running: false,
|
|
@@ -963,19 +1084,24 @@ export class ChatSession {
|
|
|
963
1084
|
effectiveRuntimeMode: undefined,
|
|
964
1085
|
permissionMode: undefined
|
|
965
1086
|
};
|
|
1087
|
+
|
|
966
1088
|
// No process of the host that went down is left to run what it kept in the background.
|
|
967
1089
|
if (info.background?.length) {
|
|
968
1090
|
for (const command of info.background) {
|
|
969
1091
|
const text = `The machine restarted while background command "${commandLabel(command)}" was running. It will not send a result. Do not wait for it.`;
|
|
970
1092
|
this.deliverNote({ noteId: `lost-command-${command.id}`, note: text, preamble: text });
|
|
971
1093
|
}
|
|
1094
|
+
|
|
972
1095
|
patch.background = [];
|
|
973
1096
|
}
|
|
1097
|
+
|
|
974
1098
|
// The window stored is whatever the session that went left behind; the next CLI starts on the pick.
|
|
975
1099
|
const contextWindow = this.options.provider.catalog.contextWindowFor(selection);
|
|
1100
|
+
|
|
976
1101
|
if (contextWindow !== info.usage.contextWindow) {
|
|
977
1102
|
patch.usage = { ...info.usage, contextWindow };
|
|
978
1103
|
}
|
|
1104
|
+
|
|
979
1105
|
if (
|
|
980
1106
|
JSON.stringify(selection) !== JSON.stringify(info.selection) ||
|
|
981
1107
|
patch.usage !== undefined ||
|
|
@@ -988,6 +1114,7 @@ export class ChatSession {
|
|
|
988
1114
|
) {
|
|
989
1115
|
events.push(this.thread.patchInfo(patch));
|
|
990
1116
|
}
|
|
1117
|
+
|
|
991
1118
|
this.emit(events);
|
|
992
1119
|
}
|
|
993
1120
|
|
|
@@ -1001,55 +1128,75 @@ export class ChatSession {
|
|
|
1001
1128
|
if (!this.awaitsResume(turnId, attempt)) {
|
|
1002
1129
|
return;
|
|
1003
1130
|
}
|
|
1131
|
+
|
|
1004
1132
|
const original = this.thread.get(turnId);
|
|
1133
|
+
|
|
1005
1134
|
if (original?.kind !== 'turn') {
|
|
1006
1135
|
return;
|
|
1007
1136
|
}
|
|
1137
|
+
|
|
1008
1138
|
const now = Date.now();
|
|
1139
|
+
|
|
1009
1140
|
this.emit([
|
|
1010
1141
|
this.thread.upsert({ ...original, attempt, resumePending: true }),
|
|
1011
1142
|
this.thread.upsert({ id: `resume-${turnId}-${attempt}`, kind: 'note', createdAt: now, turnId, level: 'info', text: words.note })
|
|
1012
1143
|
]);
|
|
1144
|
+
|
|
1013
1145
|
this.options.persist();
|
|
1014
1146
|
const pending = this.watchStart(turnId);
|
|
1015
1147
|
let backend: ChatBackend;
|
|
1148
|
+
|
|
1016
1149
|
try {
|
|
1017
1150
|
await this.options.save?.();
|
|
1151
|
+
|
|
1018
1152
|
if (this.pendingStart !== pending || !this.awaitsResume(turnId, attempt)) {
|
|
1019
1153
|
return;
|
|
1020
1154
|
}
|
|
1155
|
+
|
|
1021
1156
|
backend = await this.ensureBackend();
|
|
1022
1157
|
} catch (e) {
|
|
1023
1158
|
if (this.pendingStart !== pending) {
|
|
1024
1159
|
return;
|
|
1025
1160
|
}
|
|
1161
|
+
|
|
1026
1162
|
this.clearStartWarning();
|
|
1163
|
+
|
|
1027
1164
|
if (this.backend === null) {
|
|
1028
1165
|
this.emit([this.thread.patchInfo({ running: false })]);
|
|
1029
1166
|
}
|
|
1167
|
+
|
|
1030
1168
|
this.addNote('warning', `The resume attempt could not start: ${errorText(e)}`);
|
|
1169
|
+
|
|
1031
1170
|
throw e;
|
|
1032
1171
|
}
|
|
1172
|
+
|
|
1033
1173
|
const turn = this.thread.get(turnId);
|
|
1174
|
+
|
|
1034
1175
|
if (this.pendingStart !== pending || !this.awaitsResume(turnId, attempt) || this.backend !== backend || turn?.kind !== 'turn') {
|
|
1035
1176
|
return;
|
|
1036
1177
|
}
|
|
1178
|
+
|
|
1037
1179
|
this.clearStartWarning();
|
|
1038
1180
|
// The turn keeps the checkpoint it started with, so its card still shows everything it changed.
|
|
1039
1181
|
this.turnReady = Promise.resolve();
|
|
1040
1182
|
const context = this.contextNote(words.prompt);
|
|
1041
1183
|
const preamble = [words.preamble, context.preamble].filter(Boolean).join('\n\n') || null;
|
|
1184
|
+
|
|
1042
1185
|
if (context.note) {
|
|
1043
1186
|
this.emit([this.thread.upsert({ id: newId('note'), kind: 'note', createdAt: now, turnId, level: 'info', text: context.note })]);
|
|
1044
1187
|
}
|
|
1188
|
+
|
|
1045
1189
|
const input: TurnInput = { promptId: randomUUID(), text: words.prompt, preamble, attachments: [], mentions: [], skills: [] };
|
|
1046
1190
|
this.submission = { turnId, input };
|
|
1047
1191
|
this.options.persist();
|
|
1048
1192
|
await this.options.save?.();
|
|
1193
|
+
|
|
1049
1194
|
if (this.backend !== backend || this.info.activeTurnId !== turnId || this.frozen) {
|
|
1050
1195
|
return;
|
|
1051
1196
|
}
|
|
1197
|
+
|
|
1052
1198
|
backend.sendTurn(input);
|
|
1199
|
+
|
|
1053
1200
|
if (!backend.acknowledgesTurns) {
|
|
1054
1201
|
this.acceptPrompt(input.promptId!);
|
|
1055
1202
|
}
|
|
@@ -1058,16 +1205,20 @@ export class ChatSession {
|
|
|
1058
1205
|
/* Ends a running turn nobody is working on and keeps queued messages for a person's next send. */
|
|
1059
1206
|
abandon(turnId: string, reason: string | null): void {
|
|
1060
1207
|
const turn = this.thread.get(turnId);
|
|
1208
|
+
|
|
1061
1209
|
if (turn?.kind !== 'turn' || turn.state !== 'running' || this.thread.info.activeTurnId !== turnId || this.running) {
|
|
1062
1210
|
return;
|
|
1063
1211
|
}
|
|
1212
|
+
|
|
1064
1213
|
const now = Date.now();
|
|
1065
1214
|
this.pauseQueue(true);
|
|
1215
|
+
|
|
1066
1216
|
this.emit([
|
|
1067
1217
|
this.thread.upsert({ ...turn, state: 'aborted', endedAt: now }),
|
|
1068
1218
|
...(reason === null ? [] : [this.thread.upsert({ id: newId('note'), kind: 'note', createdAt: now, turnId, level: 'warning', text: reason })]),
|
|
1069
1219
|
this.thread.patchInfo({ status: 'idle', activeTurnId: null })
|
|
1070
1220
|
]);
|
|
1221
|
+
|
|
1071
1222
|
this.options.persist();
|
|
1072
1223
|
this.drainQueue(this.sendAfterStop);
|
|
1073
1224
|
this.sendAfterStop = false;
|
|
@@ -1078,16 +1229,28 @@ export class ChatSession {
|
|
|
1078
1229
|
* turn carrying what woke it, so a turn it opens cannot itself wake anyone. A caller passes `note`
|
|
1079
1230
|
* only when its news is not already written in the thread. Checked and opened in one step, so null
|
|
1080
1231
|
* means a turn (or a shutdown) was in the way. Never through `send`, which would step on a turn
|
|
1081
|
-
* with `origin: 'agent'` and close the wake it just made.
|
|
1232
|
+
* with `origin: 'agent'` and close the wake it just made. `mentions` and `skills` reach a CLI that
|
|
1233
|
+
* expands them itself, as a person's picks in a message do.
|
|
1082
1234
|
*/
|
|
1083
|
-
wake(wake: {
|
|
1235
|
+
wake(wake: {
|
|
1236
|
+
text: string;
|
|
1237
|
+
label: string;
|
|
1238
|
+
note?: string;
|
|
1239
|
+
taskIds: string[];
|
|
1240
|
+
summaryFor?: string;
|
|
1241
|
+
messageFrom?: string[];
|
|
1242
|
+
mentions?: string[];
|
|
1243
|
+
skills?: string[];
|
|
1244
|
+
}): string | null {
|
|
1084
1245
|
if (this.frozen || this.relocating || this.thread.info.activeTurnId !== null || this.replacementWaits()) {
|
|
1085
1246
|
return null;
|
|
1086
1247
|
}
|
|
1248
|
+
|
|
1087
1249
|
this.turnOpened();
|
|
1088
1250
|
const { preamble, note } = this.contextNote(wake.text);
|
|
1089
1251
|
const turnId = newId('turn');
|
|
1090
1252
|
const now = Date.now();
|
|
1253
|
+
|
|
1091
1254
|
this.emit([
|
|
1092
1255
|
this.thread.upsert({
|
|
1093
1256
|
id: turnId,
|
|
@@ -1110,9 +1273,11 @@ export class ChatSession {
|
|
|
1110
1273
|
...(note === null ? [] : [this.thread.upsert({ id: newId('note'), kind: 'note', createdAt: now, turnId, level: 'info', text: note })]),
|
|
1111
1274
|
this.thread.patchInfo({ status: this.thread.statusFor(turnId), activeTurnId: turnId })
|
|
1112
1275
|
]);
|
|
1276
|
+
|
|
1113
1277
|
this.options.persist();
|
|
1114
1278
|
this.turnReady = this.checkpoint(turnId);
|
|
1115
|
-
this.submit(turnId, { text: wake.text, preamble, attachments: [], mentions: [], skills: [] });
|
|
1279
|
+
this.submit(turnId, { text: wake.text, preamble, attachments: [], mentions: wake.mentions ?? [], skills: wake.skills ?? [] });
|
|
1280
|
+
|
|
1116
1281
|
return turnId;
|
|
1117
1282
|
}
|
|
1118
1283
|
|
|
@@ -1133,20 +1298,27 @@ export class ChatSession {
|
|
|
1133
1298
|
*/
|
|
1134
1299
|
takeUpAfterLimit(turnId: string): 'opened' | 'wait' | 'lapsed' {
|
|
1135
1300
|
const turn = limitedTurn(this.thread.list());
|
|
1301
|
+
|
|
1136
1302
|
if (turn === null || turn.id !== turnId || !this.resumeAllowed()) {
|
|
1137
1303
|
this.lapseResume();
|
|
1304
|
+
|
|
1138
1305
|
return 'lapsed';
|
|
1139
1306
|
}
|
|
1307
|
+
|
|
1140
1308
|
const wake = limitResumeWake(turn.limit.kind);
|
|
1309
|
+
|
|
1141
1310
|
const opened = this.wake({
|
|
1142
1311
|
...wake,
|
|
1143
1312
|
taskIds: turn.taskIds ?? [],
|
|
1144
1313
|
...(turn.messageFrom === undefined ? {} : { messageFrom: turn.messageFrom })
|
|
1145
1314
|
});
|
|
1315
|
+
|
|
1146
1316
|
if (opened === null) {
|
|
1147
1317
|
return 'wait';
|
|
1148
1318
|
}
|
|
1319
|
+
|
|
1149
1320
|
this.pauseQueue(false);
|
|
1321
|
+
|
|
1150
1322
|
return 'opened';
|
|
1151
1323
|
}
|
|
1152
1324
|
|
|
@@ -1172,17 +1344,23 @@ export class ChatSession {
|
|
|
1172
1344
|
private oweResume(fresh: boolean): void {
|
|
1173
1345
|
const hooks = this.options.limitResume;
|
|
1174
1346
|
const turn = limitedTurn(this.thread.list());
|
|
1347
|
+
|
|
1175
1348
|
if (!hooks || turn === null || this.thread.info.activeTurnId !== null || this.frozen || !this.resumeAllowed() || this.wentOn(turn)) {
|
|
1176
1349
|
return;
|
|
1177
1350
|
}
|
|
1351
|
+
|
|
1178
1352
|
const now = hooks.now();
|
|
1353
|
+
|
|
1179
1354
|
if (!fresh && (turn.limit.kind !== 'usage' || (turn.limit.resetsAt ?? 0) <= now)) {
|
|
1180
1355
|
return;
|
|
1181
1356
|
}
|
|
1357
|
+
|
|
1182
1358
|
const at = limitResumeAt(this.thread.list(), turn, now);
|
|
1359
|
+
|
|
1183
1360
|
if (at === null || at === this.thread.info.resumeAt) {
|
|
1184
1361
|
return;
|
|
1185
1362
|
}
|
|
1363
|
+
|
|
1186
1364
|
this.emit([this.thread.patchInfo({ resumeAt: at })]);
|
|
1187
1365
|
this.options.persist();
|
|
1188
1366
|
void hooks.owe(turn.id, at).catch((e: unknown) => console.error(`Owing a resume of chat ${this.id} failed:`, errorText(e)));
|
|
@@ -1195,15 +1373,20 @@ export class ChatSession {
|
|
|
1195
1373
|
settleOwedResume(): void {
|
|
1196
1374
|
const hooks = this.options.limitResume;
|
|
1197
1375
|
const at = this.thread.info.resumeAt;
|
|
1376
|
+
|
|
1198
1377
|
if (at === undefined || hooks?.owed() === true) {
|
|
1199
1378
|
return;
|
|
1200
1379
|
}
|
|
1380
|
+
|
|
1201
1381
|
const turn = limitedTurn(this.thread.list());
|
|
1382
|
+
|
|
1202
1383
|
if (!hooks || turn === null || this.thread.info.activeTurnId !== null || !this.resumeAllowed()) {
|
|
1203
1384
|
this.emit([this.thread.patchInfo({ resumeAt: undefined })]);
|
|
1204
1385
|
this.options.persist();
|
|
1386
|
+
|
|
1205
1387
|
return;
|
|
1206
1388
|
}
|
|
1389
|
+
|
|
1207
1390
|
void hooks.owe(turn.id, at).catch((e: unknown) => console.error(`Owing a resume of chat ${this.id} failed:`, errorText(e)));
|
|
1208
1391
|
}
|
|
1209
1392
|
|
|
@@ -1212,6 +1395,7 @@ export class ChatSession {
|
|
|
1212
1395
|
if (this.thread.info.limit !== undefined) {
|
|
1213
1396
|
this.emit([this.thread.patchInfo({ limit: undefined })]);
|
|
1214
1397
|
}
|
|
1398
|
+
|
|
1215
1399
|
this.lapseResume();
|
|
1216
1400
|
}
|
|
1217
1401
|
|
|
@@ -1222,9 +1406,11 @@ export class ChatSession {
|
|
|
1222
1406
|
continuedElsewhere(note: string): void {
|
|
1223
1407
|
this.lapseResume();
|
|
1224
1408
|
const turn = limitedTurn(this.thread.list());
|
|
1409
|
+
|
|
1225
1410
|
if (turn === null || this.wentOn(turn)) {
|
|
1226
1411
|
return;
|
|
1227
1412
|
}
|
|
1413
|
+
|
|
1228
1414
|
this.emit([this.thread.upsert({ id: continuedNoteId(turn.id), kind: 'note', createdAt: Date.now(), turnId: turn.id, level: 'info', text: note })]);
|
|
1229
1415
|
this.options.persist();
|
|
1230
1416
|
}
|
|
@@ -1232,9 +1418,11 @@ export class ChatSession {
|
|
|
1232
1418
|
/* Drops the resume owed after a limit, the time the thread shows for it included. */
|
|
1233
1419
|
lapseResume(): void {
|
|
1234
1420
|
const hooks = this.options.limitResume;
|
|
1421
|
+
|
|
1235
1422
|
if (this.thread.info.resumeAt === undefined || !hooks) {
|
|
1236
1423
|
return;
|
|
1237
1424
|
}
|
|
1425
|
+
|
|
1238
1426
|
this.emit([this.thread.patchInfo({ resumeAt: undefined })]);
|
|
1239
1427
|
this.options.persist();
|
|
1240
1428
|
void hooks.lapse().catch((e: unknown) => console.error(`Dropping the resume of chat ${this.id} failed:`, errorText(e)));
|
|
@@ -1249,7 +1437,9 @@ export class ChatSession {
|
|
|
1249
1437
|
if (this.thread.get(delivery.noteId) !== undefined) {
|
|
1250
1438
|
return false;
|
|
1251
1439
|
}
|
|
1440
|
+
|
|
1252
1441
|
this.pendingPreambles = [...this.pendingPreambles, delivery.preamble];
|
|
1442
|
+
|
|
1253
1443
|
this.emit([
|
|
1254
1444
|
this.thread.upsert({
|
|
1255
1445
|
id: delivery.noteId,
|
|
@@ -1261,16 +1451,20 @@ export class ChatSession {
|
|
|
1261
1451
|
...(delivery.from === undefined ? {} : { from: delivery.from })
|
|
1262
1452
|
})
|
|
1263
1453
|
]);
|
|
1454
|
+
|
|
1264
1455
|
this.options.persist();
|
|
1456
|
+
|
|
1265
1457
|
return true;
|
|
1266
1458
|
}
|
|
1267
1459
|
|
|
1268
1460
|
/* Writes items a host draws in the thread itself (a row for work it delegated), leaving the ones that did not change alone. */
|
|
1269
1461
|
upsertItems(items: readonly ChatItem[]): void {
|
|
1270
1462
|
const changed = items.filter((item) => JSON.stringify(this.thread.get(item.id)) !== JSON.stringify(item));
|
|
1463
|
+
|
|
1271
1464
|
if (changed.length === 0) {
|
|
1272
1465
|
return;
|
|
1273
1466
|
}
|
|
1467
|
+
|
|
1274
1468
|
this.emit(changed.map((item) => this.thread.upsert(item)));
|
|
1275
1469
|
this.options.persistSoon();
|
|
1276
1470
|
}
|
|
@@ -1300,6 +1494,7 @@ export class ChatSession {
|
|
|
1300
1494
|
const now = Date.now();
|
|
1301
1495
|
const turnId = this.thread.info.activeTurnId;
|
|
1302
1496
|
const events: ChatEvent[] = [];
|
|
1497
|
+
|
|
1303
1498
|
for (const item of this.thread.list()) {
|
|
1304
1499
|
const settled =
|
|
1305
1500
|
item.kind === 'turn' && item.state === 'running'
|
|
@@ -1307,13 +1502,16 @@ export class ChatSession {
|
|
|
1307
1502
|
: item.kind === 'subagent' && item.origin !== HOST_TASK_ORIGIN && item.status === 'running'
|
|
1308
1503
|
? { ...item, status: 'failed' as const, finishedAt: now }
|
|
1309
1504
|
: settleStoredItem(item, null);
|
|
1505
|
+
|
|
1310
1506
|
if (settled !== item) {
|
|
1311
1507
|
events.push(this.thread.upsert(settled));
|
|
1312
1508
|
}
|
|
1313
1509
|
}
|
|
1510
|
+
|
|
1314
1511
|
if (turnId !== null) {
|
|
1315
1512
|
events.push(this.thread.upsert({ id: newId(`${ENDED_NOTE_PREFIX}note`), kind: 'note', createdAt: now, turnId, level: 'warning', text: reason }));
|
|
1316
1513
|
}
|
|
1514
|
+
|
|
1317
1515
|
events.push(
|
|
1318
1516
|
this.thread.patchInfo({
|
|
1319
1517
|
running: false,
|
|
@@ -1325,6 +1523,7 @@ export class ChatSession {
|
|
|
1325
1523
|
...(this.thread.info.background?.length ? { background: [] } : {})
|
|
1326
1524
|
})
|
|
1327
1525
|
);
|
|
1526
|
+
|
|
1328
1527
|
this.emit(events);
|
|
1329
1528
|
this.options.persist();
|
|
1330
1529
|
}
|
|
@@ -1338,6 +1537,7 @@ export class ChatSession {
|
|
|
1338
1537
|
if (this.thread.info.activeTurnId !== null || this.processInUse()) {
|
|
1339
1538
|
throw new ChatError('chat-busy', `Chat ${this.id} is still working; stop it before restarting its agent session`);
|
|
1340
1539
|
}
|
|
1540
|
+
|
|
1341
1541
|
return this.dropProcess();
|
|
1342
1542
|
}
|
|
1343
1543
|
|
|
@@ -1355,6 +1555,7 @@ export class ChatSession {
|
|
|
1355
1555
|
// What the init frame of the process that goes announced; a skill added since would be filtered out by it.
|
|
1356
1556
|
this.emit([this.thread.patchInfo({ running: false, effectiveRuntimeMode: undefined, permissionMode: undefined, skills: undefined })]);
|
|
1357
1557
|
this.options.persist();
|
|
1558
|
+
|
|
1358
1559
|
return backend?.dispose() ?? Promise.resolve();
|
|
1359
1560
|
}
|
|
1360
1561
|
|
|
@@ -1372,50 +1573,63 @@ export class ChatSession {
|
|
|
1372
1573
|
this.uiReplies.clear();
|
|
1373
1574
|
this.clearStartWarning();
|
|
1374
1575
|
this.clearWorkflowWarnings();
|
|
1576
|
+
|
|
1375
1577
|
if (this.titleTimer !== null) {
|
|
1376
1578
|
clearTimeout(this.titleTimer);
|
|
1377
1579
|
this.titleTimer = null;
|
|
1378
1580
|
}
|
|
1581
|
+
|
|
1379
1582
|
const ended = this.backend?.dispose() ?? Promise.resolve();
|
|
1380
1583
|
this.backend = null;
|
|
1381
1584
|
this.starting = null;
|
|
1585
|
+
|
|
1382
1586
|
return Promise.all([ended, ...this.checkpointWork]).then(() => undefined);
|
|
1383
1587
|
}
|
|
1384
1588
|
|
|
1385
1589
|
/* What a turn changed against its checkpoint: the stored answer, or one taken now while it runs. */
|
|
1386
1590
|
async turnDiff(turnId: string): Promise<ChatCheckpointDiff | null> {
|
|
1387
1591
|
const turn = this.thread.get(turnId);
|
|
1592
|
+
|
|
1388
1593
|
if (turn?.kind !== 'turn') {
|
|
1389
1594
|
throw new ChatError('request-not-found', `No turn ${turnId} in chat ${this.id}`);
|
|
1390
1595
|
}
|
|
1596
|
+
|
|
1391
1597
|
if (turn.checkpointDiff) {
|
|
1392
1598
|
return turn.checkpointDiff;
|
|
1393
1599
|
}
|
|
1600
|
+
|
|
1394
1601
|
if (turn.checkpoint === undefined || !this.options.checkpoints) {
|
|
1395
1602
|
return null;
|
|
1396
1603
|
}
|
|
1604
|
+
|
|
1397
1605
|
return await this.options.checkpoints.diff(this.thread.info.cwd, turn.checkpoint);
|
|
1398
1606
|
}
|
|
1399
1607
|
|
|
1400
1608
|
/* A page of a thread the CLI keeps, through the process that runs now; null when none runs that can answer. */
|
|
1401
1609
|
listThreadItems(params: { threadId: string; cursor?: string; limit: number; sortDirection: 'asc' | 'desc' }): Promise<unknown> | null {
|
|
1402
1610
|
const backend = this.backend;
|
|
1611
|
+
|
|
1403
1612
|
return backend?.running === true && backend.listThreadItems ? backend.listThreadItems(params) : null;
|
|
1404
1613
|
}
|
|
1405
1614
|
|
|
1406
1615
|
/* A completed transcript survives the process; unfinished work cannot send a result after a restart. */
|
|
1407
1616
|
async settleOrphanedSubagents(): Promise<void> {
|
|
1408
1617
|
const rows = this.runningBackgroundRows();
|
|
1618
|
+
|
|
1409
1619
|
if (this.info.provider === 'claude') {
|
|
1410
1620
|
for (const row of rows) {
|
|
1411
1621
|
this.orphans.add(row.toolUseId);
|
|
1412
1622
|
}
|
|
1623
|
+
|
|
1413
1624
|
await this.settleFromTranscripts(rows);
|
|
1414
1625
|
}
|
|
1626
|
+
|
|
1415
1627
|
const lost = this.runningBackgroundRows();
|
|
1628
|
+
|
|
1416
1629
|
for (const row of lost) {
|
|
1417
1630
|
this.recordLostBackground(row);
|
|
1418
1631
|
}
|
|
1632
|
+
|
|
1419
1633
|
this.settleRows(lost.map((row) => ({ ...row, status: 'failed', finishedAt: Date.now() })));
|
|
1420
1634
|
}
|
|
1421
1635
|
|
|
@@ -1434,15 +1648,20 @@ export class ChatSession {
|
|
|
1434
1648
|
const row = this.thread.find('subagent', (item) => item.toolUseId === toolUseId);
|
|
1435
1649
|
const agentId = row?.native?.agentId;
|
|
1436
1650
|
const owned = this.subagentOwners.get(toolUseId) === this.generation;
|
|
1651
|
+
|
|
1437
1652
|
if (agentId !== undefined && owned && this.backend?.stopTask) {
|
|
1438
1653
|
this.stopping.set(agentId, toolUseId);
|
|
1439
1654
|
this.backend.stopTask(agentId);
|
|
1655
|
+
|
|
1440
1656
|
return 'stopping';
|
|
1441
1657
|
}
|
|
1658
|
+
|
|
1442
1659
|
if (agentId !== undefined && !owned && this.thread.info.activeTurnId !== null) {
|
|
1443
1660
|
throw new ChatError('subagent-stale', 'This sub-agent belongs to a process that has ended');
|
|
1444
1661
|
}
|
|
1662
|
+
|
|
1445
1663
|
this.markSubagentStopped(toolUseId);
|
|
1664
|
+
|
|
1446
1665
|
return 'marked';
|
|
1447
1666
|
}
|
|
1448
1667
|
|
|
@@ -1451,6 +1670,7 @@ export class ChatSession {
|
|
|
1451
1670
|
this.subagentOwners.set(event.ref, generation);
|
|
1452
1671
|
} else if (event.type === 'task.done') {
|
|
1453
1672
|
this.noteStopped(event);
|
|
1673
|
+
|
|
1454
1674
|
if (event.ref !== null) {
|
|
1455
1675
|
this.subagentOwners.delete(event.ref);
|
|
1456
1676
|
}
|
|
@@ -1462,18 +1682,24 @@ export class ChatSession {
|
|
|
1462
1682
|
/* The CLI would not stop it: with no turn running the row is only marked, and while one runs a note says it still goes on. */
|
|
1463
1683
|
private refuseStop(agentId: string, message: string): void {
|
|
1464
1684
|
const toolUseId = this.stopping.get(agentId);
|
|
1685
|
+
|
|
1465
1686
|
if (toolUseId === undefined) {
|
|
1466
1687
|
return;
|
|
1467
1688
|
}
|
|
1689
|
+
|
|
1468
1690
|
this.stopping.delete(agentId);
|
|
1469
1691
|
const row = this.thread.find('subagent', (item) => item.toolUseId === toolUseId);
|
|
1692
|
+
|
|
1470
1693
|
if (row?.status !== 'running') {
|
|
1471
1694
|
return;
|
|
1472
1695
|
}
|
|
1696
|
+
|
|
1473
1697
|
if (this.thread.info.activeTurnId === null) {
|
|
1474
1698
|
this.markSubagentStopped(toolUseId);
|
|
1699
|
+
|
|
1475
1700
|
return;
|
|
1476
1701
|
}
|
|
1702
|
+
|
|
1477
1703
|
this.emit([
|
|
1478
1704
|
this.thread.upsert({
|
|
1479
1705
|
id: `stop-refused-${toolUseId}-${Date.now()}`,
|
|
@@ -1484,25 +1710,33 @@ export class ChatSession {
|
|
|
1484
1710
|
text: `"${row.description || row.subagentType || 'Sub-agent'}" could not be stopped: ${message}`
|
|
1485
1711
|
})
|
|
1486
1712
|
]);
|
|
1713
|
+
|
|
1487
1714
|
this.options.persist();
|
|
1488
1715
|
}
|
|
1489
1716
|
|
|
1490
1717
|
/* The line under a subagent that `stop_task` ended, once the CLI reported it settled. */
|
|
1491
1718
|
private noteStopped(event: Extract<BackendEvent, { type: 'task.done' }>): void {
|
|
1492
1719
|
const stopped = [...this.stopping].find(([agentId, toolUseId]) => agentId === event.taskId || toolUseId === event.ref);
|
|
1720
|
+
|
|
1493
1721
|
if (stopped === undefined) {
|
|
1494
1722
|
return;
|
|
1495
1723
|
}
|
|
1724
|
+
|
|
1496
1725
|
this.stopping.delete(stopped[0]);
|
|
1726
|
+
|
|
1497
1727
|
// It finished on its own just before the stop arrived; the row already says so.
|
|
1498
1728
|
if (event.ok) {
|
|
1499
1729
|
return;
|
|
1500
1730
|
}
|
|
1731
|
+
|
|
1501
1732
|
const row = this.thread.find('subagent', (item) => item.toolUseId === stopped[1]);
|
|
1733
|
+
|
|
1502
1734
|
if (!row) {
|
|
1503
1735
|
return;
|
|
1504
1736
|
}
|
|
1737
|
+
|
|
1505
1738
|
const now = Date.now();
|
|
1739
|
+
|
|
1506
1740
|
this.emit([
|
|
1507
1741
|
...(row.status === 'running' ? [this.thread.upsert({ ...row, status: 'failed' as const, finishedAt: now })] : []),
|
|
1508
1742
|
this.thread.upsert({
|
|
@@ -1514,6 +1748,7 @@ export class ChatSession {
|
|
|
1514
1748
|
text: `"${row.description || row.subagentType || 'Sub-agent'}" was stopped.`
|
|
1515
1749
|
})
|
|
1516
1750
|
]);
|
|
1751
|
+
|
|
1517
1752
|
this.options.persist();
|
|
1518
1753
|
}
|
|
1519
1754
|
|
|
@@ -1525,10 +1760,13 @@ export class ChatSession {
|
|
|
1525
1760
|
if (this.thread.info.activeTurnId !== null) {
|
|
1526
1761
|
throw new ChatError('chat-busy', 'A turn is running; stop the turn instead');
|
|
1527
1762
|
}
|
|
1763
|
+
|
|
1528
1764
|
const row = this.thread.find('subagent', (item) => item.toolUseId === toolUseId);
|
|
1765
|
+
|
|
1529
1766
|
if (row?.status !== 'running') {
|
|
1530
1767
|
return;
|
|
1531
1768
|
}
|
|
1769
|
+
|
|
1532
1770
|
this.markStopped([row]);
|
|
1533
1771
|
}
|
|
1534
1772
|
|
|
@@ -1545,11 +1783,15 @@ export class ChatSession {
|
|
|
1545
1783
|
if (rows.length === 0) {
|
|
1546
1784
|
return;
|
|
1547
1785
|
}
|
|
1786
|
+
|
|
1548
1787
|
const now = Date.now();
|
|
1788
|
+
|
|
1549
1789
|
for (const row of rows) {
|
|
1550
1790
|
this.orphans.delete(row.toolUseId);
|
|
1551
1791
|
}
|
|
1792
|
+
|
|
1552
1793
|
const names = rows.map((row) => `"${row.description || row.subagentType || 'Sub-agent'}"`).join(', ');
|
|
1794
|
+
|
|
1553
1795
|
this.emit([
|
|
1554
1796
|
...rows.map((row) => this.thread.upsert({ ...row, status: 'failed', finishedAt: now })),
|
|
1555
1797
|
this.thread.upsert({
|
|
@@ -1561,12 +1803,14 @@ export class ChatSession {
|
|
|
1561
1803
|
text: `${names} ${rows.length === 1 ? 'was' : 'were'} marked as stopped. ${this.options.provider.name} cannot stop one sub-agent on its own, so ${rows.length === 1 ? 'it' : 'they'} may keep working until the chat's process ends.`
|
|
1562
1804
|
})
|
|
1563
1805
|
]);
|
|
1806
|
+
|
|
1564
1807
|
this.options.persist();
|
|
1565
1808
|
}
|
|
1566
1809
|
|
|
1567
1810
|
/* Asks the transcript again for a row a load left running, when somebody looks at it. */
|
|
1568
1811
|
async recheckOrphan(toolUseId: string): Promise<void> {
|
|
1569
1812
|
const row = this.thread.find('subagent', (item) => item.toolUseId === toolUseId);
|
|
1813
|
+
|
|
1570
1814
|
if (this.orphans.has(toolUseId) && (row?.status === 'running' || row?.status === 'failed')) {
|
|
1571
1815
|
await this.settleFromTranscripts([row]);
|
|
1572
1816
|
}
|
|
@@ -1575,9 +1819,11 @@ export class ChatSession {
|
|
|
1575
1819
|
/* Writes down where a subagent's conversation was found, so the next question about it reads no folder. */
|
|
1576
1820
|
noteSubagentNative(toolUseId: string, native: { agentId?: string; threadId?: string }): void {
|
|
1577
1821
|
const item = this.thread.find('subagent', (candidate) => candidate.toolUseId === toolUseId);
|
|
1822
|
+
|
|
1578
1823
|
if (!item || (item.native?.agentId === native.agentId && item.native?.threadId === native.threadId)) {
|
|
1579
1824
|
return;
|
|
1580
1825
|
}
|
|
1826
|
+
|
|
1581
1827
|
this.emit([this.thread.upsert({ ...item, native: { ...item.native, ...native } })]);
|
|
1582
1828
|
this.options.persistSoon();
|
|
1583
1829
|
}
|
|
@@ -1589,9 +1835,11 @@ export class ChatSession {
|
|
|
1589
1835
|
*/
|
|
1590
1836
|
async skills(scan: () => Promise<ChatSkill[]>): Promise<ChatSkill[]> {
|
|
1591
1837
|
const backend = this.backend;
|
|
1838
|
+
|
|
1592
1839
|
if (backend?.running === true && backend.listSkills) {
|
|
1593
1840
|
try {
|
|
1594
1841
|
const own = await backend.listSkills();
|
|
1842
|
+
|
|
1595
1843
|
if (own.length > 0) {
|
|
1596
1844
|
return own;
|
|
1597
1845
|
}
|
|
@@ -1599,12 +1847,16 @@ export class ChatSession {
|
|
|
1599
1847
|
// The CLI could not answer; the scan below is what is left.
|
|
1600
1848
|
}
|
|
1601
1849
|
}
|
|
1850
|
+
|
|
1602
1851
|
const scanned = await scan();
|
|
1603
1852
|
const announced = this.thread.info.skills;
|
|
1853
|
+
|
|
1604
1854
|
if (!announced?.length) {
|
|
1605
1855
|
return scanned;
|
|
1606
1856
|
}
|
|
1857
|
+
|
|
1607
1858
|
const known = new Set(announced);
|
|
1859
|
+
|
|
1608
1860
|
return scanned.filter((skill) => known.has(skill.name));
|
|
1609
1861
|
}
|
|
1610
1862
|
|
|
@@ -1613,14 +1865,17 @@ export class ChatSession {
|
|
|
1613
1865
|
const turnId = requestedTurnId ?? newId('turn');
|
|
1614
1866
|
const now = Date.now();
|
|
1615
1867
|
const events = [this.thread.upsert({ id: turnId, kind: 'turn', createdAt: now, turnId, state: 'running', origin: 'user', endedAt: null, costUsd: 0 })];
|
|
1868
|
+
|
|
1616
1869
|
if (note !== null) {
|
|
1617
1870
|
events.push(this.thread.upsert({ id: newId('note'), kind: 'note', createdAt: now, turnId, level: 'info', text: note }));
|
|
1618
1871
|
}
|
|
1872
|
+
|
|
1619
1873
|
if (text !== null) {
|
|
1620
1874
|
const mentions = extras.mentions?.length ? extras.mentions : undefined;
|
|
1621
1875
|
const skills = extras.skills?.length ? extras.skills : undefined;
|
|
1622
1876
|
const chats = extras.chats?.length ? extras.chats : undefined;
|
|
1623
1877
|
const attachments = extras.attachments?.length ? extras.attachments : undefined;
|
|
1878
|
+
|
|
1624
1879
|
events.push(
|
|
1625
1880
|
this.thread.upsert({
|
|
1626
1881
|
id: newId('user'),
|
|
@@ -1638,13 +1893,17 @@ export class ChatSession {
|
|
|
1638
1893
|
})
|
|
1639
1894
|
);
|
|
1640
1895
|
}
|
|
1896
|
+
|
|
1641
1897
|
events.push(this.thread.patchInfo({ status: this.thread.statusFor(turnId), activeTurnId: turnId }));
|
|
1642
1898
|
this.emit(events);
|
|
1899
|
+
|
|
1643
1900
|
if (extras.uiChoice) {
|
|
1644
1901
|
this.recordUiAnswer({ ...extras.uiChoice, turnId, queued: false });
|
|
1645
1902
|
}
|
|
1903
|
+
|
|
1646
1904
|
// On disk before the CLI answers, so a host that goes down mid-turn still shows the question.
|
|
1647
1905
|
this.options.persist();
|
|
1906
|
+
|
|
1648
1907
|
return turnId;
|
|
1649
1908
|
}
|
|
1650
1909
|
|
|
@@ -1655,12 +1914,15 @@ export class ChatSession {
|
|
|
1655
1914
|
private settleAgentTurn(): void {
|
|
1656
1915
|
const turnId = this.thread.info.activeTurnId;
|
|
1657
1916
|
const turn = turnId ? this.thread.get(turnId) : undefined;
|
|
1917
|
+
|
|
1658
1918
|
if (turn?.kind !== 'turn' || turn.origin !== 'agent') {
|
|
1659
1919
|
return;
|
|
1660
1920
|
}
|
|
1921
|
+
|
|
1661
1922
|
if (this.spokenTurnId === turn.id) {
|
|
1662
1923
|
this.staleResults += 1;
|
|
1663
1924
|
}
|
|
1925
|
+
|
|
1664
1926
|
this.emit([this.thread.upsert({ ...turn, state: 'done', endedAt: Date.now() }), this.thread.patchInfo({ status: 'idle', activeTurnId: null })]);
|
|
1665
1927
|
this.settleCheckpoint(turn.id);
|
|
1666
1928
|
}
|
|
@@ -1668,16 +1930,21 @@ export class ChatSession {
|
|
|
1668
1930
|
/* Records the folder's tree on the turn; a folder without git leaves the turn as it is. */
|
|
1669
1931
|
private checkpoint(turnId: string): Promise<void> {
|
|
1670
1932
|
const checkpoints = this.options.checkpoints;
|
|
1933
|
+
|
|
1671
1934
|
if (!checkpoints) {
|
|
1672
1935
|
return Promise.resolve();
|
|
1673
1936
|
}
|
|
1937
|
+
|
|
1674
1938
|
const cwd = this.thread.info.cwd;
|
|
1939
|
+
|
|
1675
1940
|
return this.trackCheckpoint(
|
|
1676
1941
|
Promise.all([checkpoints.take(cwd), checkpoints.head?.(cwd) ?? null]).then(([tree, head]) => {
|
|
1677
1942
|
const turn = this.thread.get(turnId);
|
|
1943
|
+
|
|
1678
1944
|
if (tree === null || turn?.kind !== 'turn' || turn.state !== 'running' || this.info.activeTurnId !== turnId) {
|
|
1679
1945
|
return;
|
|
1680
1946
|
}
|
|
1947
|
+
|
|
1681
1948
|
this.emit([this.thread.upsert({ ...turn, checkpoint: tree, ...(head === null ? {} : { checkpointHead: head }) })]);
|
|
1682
1949
|
})
|
|
1683
1950
|
);
|
|
@@ -1686,17 +1953,22 @@ export class ChatSession {
|
|
|
1686
1953
|
/* The diff of a turn that just ended, so a reload shows it without asking git again, and the tree a fork after it starts from. */
|
|
1687
1954
|
private settleCheckpoint(turnId: string): void {
|
|
1688
1955
|
const turn = this.thread.get(turnId);
|
|
1956
|
+
|
|
1689
1957
|
if (turn?.kind !== 'turn' || turn.checkpoint === undefined || turn.checkpointDiff || !this.options.checkpoints) {
|
|
1690
1958
|
return;
|
|
1691
1959
|
}
|
|
1960
|
+
|
|
1692
1961
|
const { checkpoints } = this.options;
|
|
1693
1962
|
const cwd = this.thread.info.cwd;
|
|
1963
|
+
|
|
1694
1964
|
void this.trackCheckpoint(
|
|
1695
1965
|
Promise.all([checkpoints.settle(cwd, turn.checkpoint), checkpoints.head?.(cwd) ?? null]).then(([answer, head]) => {
|
|
1696
1966
|
const settled = this.thread.get(turnId);
|
|
1967
|
+
|
|
1697
1968
|
if (answer === null || settled?.kind !== 'turn') {
|
|
1698
1969
|
return;
|
|
1699
1970
|
}
|
|
1971
|
+
|
|
1700
1972
|
this.emit([
|
|
1701
1973
|
this.thread.upsert({
|
|
1702
1974
|
...settled,
|
|
@@ -1705,6 +1977,7 @@ export class ChatSession {
|
|
|
1705
1977
|
...(head === null ? {} : { checkpointHeadAfter: head })
|
|
1706
1978
|
})
|
|
1707
1979
|
]);
|
|
1980
|
+
|
|
1708
1981
|
this.options.persist();
|
|
1709
1982
|
})
|
|
1710
1983
|
);
|
|
@@ -1714,6 +1987,7 @@ export class ChatSession {
|
|
|
1714
1987
|
private trackCheckpoint(work: Promise<void>): Promise<void> {
|
|
1715
1988
|
const tracked = work.catch(() => undefined).finally(() => this.checkpointWork.delete(tracked));
|
|
1716
1989
|
this.checkpointWork.add(tracked);
|
|
1990
|
+
|
|
1717
1991
|
return tracked;
|
|
1718
1992
|
}
|
|
1719
1993
|
|
|
@@ -1726,6 +2000,7 @@ export class ChatSession {
|
|
|
1726
2000
|
if (text.startsWith('/')) {
|
|
1727
2001
|
return { preamble: null, note: null };
|
|
1728
2002
|
}
|
|
2003
|
+
|
|
1729
2004
|
const host = this.options.promptNotes?.next() ?? { shown: [], heard: [] };
|
|
1730
2005
|
// Keep consumed notes in the submission until the CLI acknowledges them.
|
|
1731
2006
|
const preambles = this.preambles;
|
|
@@ -1734,7 +2009,9 @@ export class ChatSession {
|
|
|
1734
2009
|
const hints = this.pendingHints;
|
|
1735
2010
|
this.pendingHints = [];
|
|
1736
2011
|
const read = [...preambles, ...host.shown];
|
|
2012
|
+
|
|
1737
2013
|
const joined = (parts: string[]): string | null => (parts.length === 0 ? null : parts.join('\n\n'));
|
|
2014
|
+
|
|
1738
2015
|
return { preamble: joined([...read, ...host.heard, ...hints]), note: joined(read) };
|
|
1739
2016
|
}
|
|
1740
2017
|
|
|
@@ -1743,8 +2020,10 @@ export class ChatSession {
|
|
|
1743
2020
|
this.submission = { turnId, input: { ...input, promptId: randomUUID() } };
|
|
1744
2021
|
const submission = this.submission;
|
|
1745
2022
|
this.options.persist();
|
|
2023
|
+
|
|
1746
2024
|
this.run((backend) => {
|
|
1747
2025
|
backend.sendTurn(submission.input);
|
|
2026
|
+
|
|
1748
2027
|
if (!backend.acknowledgesTurns) {
|
|
1749
2028
|
this.acceptPrompt(submission.input.promptId!);
|
|
1750
2029
|
}
|
|
@@ -1753,43 +2032,54 @@ export class ChatSession {
|
|
|
1753
2032
|
|
|
1754
2033
|
private acceptPrompt(promptId: string): void {
|
|
1755
2034
|
const submission = this.submission;
|
|
2035
|
+
|
|
1756
2036
|
if (submission?.input.promptId !== promptId) {
|
|
1757
2037
|
return;
|
|
1758
2038
|
}
|
|
2039
|
+
|
|
1759
2040
|
this.submission = null;
|
|
1760
2041
|
const turn = this.thread.get(submission.turnId);
|
|
2042
|
+
|
|
1761
2043
|
if (turn?.kind === 'turn' && (turn.deliveryPending || turn.resumePending)) {
|
|
1762
2044
|
this.emit([this.thread.upsert({ ...turn, deliveryPending: undefined, resumePending: undefined })]);
|
|
1763
2045
|
}
|
|
2046
|
+
|
|
1764
2047
|
this.options.persist();
|
|
1765
2048
|
}
|
|
1766
2049
|
|
|
1767
2050
|
/* Runs one turn against the backend; a backend that will not start ends the turn with the reason. */
|
|
1768
2051
|
private run(work: (backend: ChatBackend) => void): void {
|
|
1769
2052
|
const turnId = this.info.activeTurnId;
|
|
2053
|
+
|
|
1770
2054
|
if (turnId === null) {
|
|
1771
2055
|
return;
|
|
1772
2056
|
}
|
|
2057
|
+
|
|
1773
2058
|
const pending = this.watchStart(turnId);
|
|
1774
2059
|
let backend: Promise<ChatBackend>;
|
|
2060
|
+
|
|
1775
2061
|
try {
|
|
1776
2062
|
backend = this.ensureBackend();
|
|
1777
2063
|
} catch (error) {
|
|
1778
2064
|
// What the host prepares for a launch (its folders, its instructions) can throw too; the turn already opened, so it fails.
|
|
1779
|
-
backend = Promise.reject(
|
|
2065
|
+
backend = Promise.reject(asError(error));
|
|
1780
2066
|
}
|
|
2067
|
+
|
|
1781
2068
|
const generation = this.generation;
|
|
2069
|
+
|
|
1782
2070
|
void Promise.all([backend, this.turnReady, this.options.save?.()])
|
|
1783
2071
|
.then(([started]) => {
|
|
1784
2072
|
if (this.pendingStart !== pending || this.backend !== started || this.info.activeTurnId !== turnId || this.frozen) {
|
|
1785
2073
|
return;
|
|
1786
2074
|
}
|
|
2075
|
+
|
|
1787
2076
|
this.clearStartWarning();
|
|
1788
2077
|
work(started);
|
|
1789
2078
|
})
|
|
1790
2079
|
.catch((error: unknown) => {
|
|
1791
2080
|
if (this.pendingStart === pending || (this.backend !== null && this.generation === generation && this.info.activeTurnId === turnId)) {
|
|
1792
2081
|
this.clearStartWarning();
|
|
2082
|
+
|
|
1793
2083
|
if (error instanceof ChatError && error.code === 'chat-busy') {
|
|
1794
2084
|
this.pendingPreambles = this.preambles;
|
|
1795
2085
|
this.submission = null;
|
|
@@ -1797,8 +2087,10 @@ export class ChatSession {
|
|
|
1797
2087
|
this.emit(this.projector.project(generation, { type: 'turn.done', state: 'error', costUsd: 0, error: error.message }));
|
|
1798
2088
|
this.settleCheckpoint(turnId);
|
|
1799
2089
|
this.options.persist();
|
|
2090
|
+
|
|
1800
2091
|
return;
|
|
1801
2092
|
}
|
|
2093
|
+
|
|
1802
2094
|
this.receive(generation, { type: 'failed', message: errorText(error), processAlive: this.running });
|
|
1803
2095
|
}
|
|
1804
2096
|
});
|
|
@@ -1807,6 +2099,7 @@ export class ChatSession {
|
|
|
1807
2099
|
private watchStart(turnId: string): { turnId: string; warning: unknown } {
|
|
1808
2100
|
this.clearStartWarning();
|
|
1809
2101
|
const clock = this.options.clock ?? systemClock;
|
|
2102
|
+
|
|
1810
2103
|
const pending = {
|
|
1811
2104
|
turnId,
|
|
1812
2105
|
warning: clock.setTimeout(() => {
|
|
@@ -1821,11 +2114,14 @@ export class ChatSession {
|
|
|
1821
2114
|
text: 'The agent is still starting. You can stop this turn and try again.'
|
|
1822
2115
|
})
|
|
1823
2116
|
]);
|
|
2117
|
+
|
|
1824
2118
|
this.options.persist();
|
|
1825
2119
|
}
|
|
1826
2120
|
}, 30_000)
|
|
1827
2121
|
};
|
|
2122
|
+
|
|
1828
2123
|
this.pendingStart = pending;
|
|
2124
|
+
|
|
1829
2125
|
return pending;
|
|
1830
2126
|
}
|
|
1831
2127
|
|
|
@@ -1833,6 +2129,7 @@ export class ChatSession {
|
|
|
1833
2129
|
if (this.pendingStart === null) {
|
|
1834
2130
|
return;
|
|
1835
2131
|
}
|
|
2132
|
+
|
|
1836
2133
|
(this.options.clock ?? systemClock).clearTimeout(this.pendingStart.warning);
|
|
1837
2134
|
this.pendingStart = null;
|
|
1838
2135
|
}
|
|
@@ -1871,27 +2168,34 @@ export class ChatSession {
|
|
|
1871
2168
|
if (this.processInUse()) {
|
|
1872
2169
|
return Promise.reject(new ChatError('chat-busy', 'The current work and open requests must finish before the changes to this chat can apply'));
|
|
1873
2170
|
}
|
|
2171
|
+
|
|
1874
2172
|
// Let the old one go quietly; its events are dropped and its exit must not end the chat.
|
|
1875
2173
|
this.backend.stop();
|
|
1876
2174
|
this.backend = null;
|
|
1877
2175
|
this.starting = null;
|
|
1878
2176
|
}
|
|
2177
|
+
|
|
1879
2178
|
if (this.backend && this.starting) {
|
|
1880
2179
|
return this.starting;
|
|
1881
2180
|
}
|
|
2181
|
+
|
|
1882
2182
|
const projectConfig = this.loadsProjectConfig();
|
|
2183
|
+
|
|
1883
2184
|
if (!projectConfig && !this.options.provider.ignoresProjectConfig) {
|
|
1884
2185
|
return Promise.reject(
|
|
1885
2186
|
new ChatError('project-config-unsupported', `${this.options.provider.name} cannot run without the configuration of its folder`)
|
|
1886
2187
|
);
|
|
1887
2188
|
}
|
|
2189
|
+
|
|
1888
2190
|
let env: Record<string, string>;
|
|
2191
|
+
|
|
1889
2192
|
try {
|
|
1890
2193
|
env = this.options.env(this.thread.info.account);
|
|
1891
2194
|
this.launchedAccount = this.thread.info.account;
|
|
1892
2195
|
} catch (error) {
|
|
1893
|
-
return Promise.reject(
|
|
2196
|
+
return Promise.reject(asError(error));
|
|
1894
2197
|
}
|
|
2198
|
+
|
|
1895
2199
|
this.generation = nextGeneration();
|
|
1896
2200
|
const generation = this.generation;
|
|
1897
2201
|
const info = this.thread.info;
|
|
@@ -1901,6 +2205,7 @@ export class ChatSession {
|
|
|
1901
2205
|
const folders = this.options.folders?.() ?? [];
|
|
1902
2206
|
this.launchedFolders = folders;
|
|
1903
2207
|
this.launchedProjectConfig = projectConfig;
|
|
2208
|
+
|
|
1904
2209
|
const launch: BackendLaunch = {
|
|
1905
2210
|
command: this.options.command,
|
|
1906
2211
|
cwd: info.cwd,
|
|
@@ -1921,7 +2226,9 @@ export class ChatSession {
|
|
|
1921
2226
|
...(projectConfig ? {} : { projectConfig }),
|
|
1922
2227
|
...(this.options.spawn ? { spawn: this.options.spawn } : {})
|
|
1923
2228
|
};
|
|
2229
|
+
|
|
1924
2230
|
const made: { backend: ChatBackend | null } = { backend: null };
|
|
2231
|
+
|
|
1925
2232
|
try {
|
|
1926
2233
|
made.backend = this.options.provider.createBackend(launch, {
|
|
1927
2234
|
onEvent: (event) => {
|
|
@@ -1931,19 +2238,22 @@ export class ChatSession {
|
|
|
1931
2238
|
}
|
|
1932
2239
|
});
|
|
1933
2240
|
} catch (error) {
|
|
1934
|
-
return Promise.reject(
|
|
2241
|
+
return Promise.reject(asError(error));
|
|
1935
2242
|
}
|
|
2243
|
+
|
|
1936
2244
|
const backend = made.backend;
|
|
1937
2245
|
this.backend = backend;
|
|
1938
2246
|
this.replaceProcess = false;
|
|
1939
2247
|
this.emit([this.thread.patchInfo({ running: true, effectiveRuntimeMode: undefined, permissionMode: undefined })]);
|
|
1940
2248
|
let started: Promise<void>;
|
|
2249
|
+
|
|
1941
2250
|
try {
|
|
1942
2251
|
started = backend.start();
|
|
1943
2252
|
} catch (error) {
|
|
1944
2253
|
// A spawn that throws on the spot (no such executable) is a start that failed like any other.
|
|
1945
|
-
started = Promise.reject(
|
|
2254
|
+
started = Promise.reject(asError(error));
|
|
1946
2255
|
}
|
|
2256
|
+
|
|
1947
2257
|
this.starting = started
|
|
1948
2258
|
.then(() => backend)
|
|
1949
2259
|
.catch((error: unknown) => {
|
|
@@ -1952,13 +2262,16 @@ export class ChatSession {
|
|
|
1952
2262
|
this.backend = null;
|
|
1953
2263
|
this.starting = null;
|
|
1954
2264
|
}
|
|
2265
|
+
|
|
1955
2266
|
throw error;
|
|
1956
2267
|
});
|
|
2268
|
+
|
|
1957
2269
|
return this.starting;
|
|
1958
2270
|
}
|
|
1959
2271
|
|
|
1960
2272
|
private awaitsResume(turnId: string, attempt: number): boolean {
|
|
1961
2273
|
const turn = this.thread.get(turnId);
|
|
2274
|
+
|
|
1962
2275
|
return (
|
|
1963
2276
|
!this.frozen &&
|
|
1964
2277
|
turn?.kind === 'turn' &&
|
|
@@ -1972,33 +2285,45 @@ export class ChatSession {
|
|
|
1972
2285
|
if (this.frozen) {
|
|
1973
2286
|
return;
|
|
1974
2287
|
}
|
|
2288
|
+
|
|
1975
2289
|
if (event.type === 'turn.accepted') {
|
|
1976
2290
|
this.acceptPrompt(event.promptId);
|
|
2291
|
+
|
|
1977
2292
|
return;
|
|
1978
2293
|
}
|
|
2294
|
+
|
|
1979
2295
|
if (event.type === 'limits') {
|
|
1980
2296
|
this.options.onLimits?.(this.launchedAccount === undefined ? event.update : { ...event.update, account: this.launchedAccount });
|
|
2297
|
+
|
|
1981
2298
|
return;
|
|
1982
2299
|
}
|
|
2300
|
+
|
|
1983
2301
|
if (event.type === 'title') {
|
|
1984
2302
|
this.applyTitle(event.title);
|
|
2303
|
+
|
|
1985
2304
|
return;
|
|
1986
2305
|
}
|
|
2306
|
+
|
|
1987
2307
|
if (event.type === 'stop.refused') {
|
|
1988
2308
|
this.refuseStop(event.taskId, event.message);
|
|
2309
|
+
|
|
1989
2310
|
return;
|
|
1990
2311
|
}
|
|
2312
|
+
|
|
1991
2313
|
/*
|
|
1992
2314
|
* A CLI that counts its tokens but names no window, in a chat that has none yet: the catalog may have learned the
|
|
1993
2315
|
* model's window since the chat began, as an ACP agent's catalog does from the first session that reports its models.
|
|
1994
2316
|
*/
|
|
1995
2317
|
if (event.type === 'usage' && event.contextWindow === undefined && event.contextTokens !== undefined && this.thread.info.usage.contextWindow === null) {
|
|
1996
2318
|
const contextWindow = this.options.provider.catalog.contextWindowFor(this.launchedSelection ?? this.thread.info.selection);
|
|
2319
|
+
|
|
1997
2320
|
if (contextWindow !== null) {
|
|
1998
2321
|
this.emit(this.projector.project(generation, { ...event, contextWindow }));
|
|
2322
|
+
|
|
1999
2323
|
return;
|
|
2000
2324
|
}
|
|
2001
2325
|
}
|
|
2326
|
+
|
|
2002
2327
|
/*
|
|
2003
2328
|
* A CLI reports the window it was started on. A pick made since waits for the restart the next
|
|
2004
2329
|
* send does, so its report may not put the old size back under a meter that already reads the new one.
|
|
@@ -2007,66 +2332,88 @@ export class ChatSession {
|
|
|
2007
2332
|
if (event.contextTokens === undefined) {
|
|
2008
2333
|
return;
|
|
2009
2334
|
}
|
|
2335
|
+
|
|
2010
2336
|
this.emit(this.projector.project(generation, { type: 'usage', contextTokens: event.contextTokens }));
|
|
2337
|
+
|
|
2011
2338
|
return;
|
|
2012
2339
|
}
|
|
2340
|
+
|
|
2013
2341
|
if (event.type === 'exit') {
|
|
2014
2342
|
this.backend = null;
|
|
2015
2343
|
this.starting = null;
|
|
2016
2344
|
this.awaitingBackgroundReport = false;
|
|
2017
2345
|
}
|
|
2346
|
+
|
|
2018
2347
|
// A turn we settled ourselves still has its own `result` coming; it may not close the turn after it.
|
|
2019
2348
|
if (event.type === 'turn.done' && event.promptId === undefined && this.staleResults > 0) {
|
|
2020
2349
|
this.staleResults -= 1;
|
|
2350
|
+
|
|
2021
2351
|
return;
|
|
2022
2352
|
}
|
|
2353
|
+
|
|
2023
2354
|
if (event.type === 'exit' || event.type === 'failed') {
|
|
2024
2355
|
this.staleResults = 0;
|
|
2025
2356
|
}
|
|
2357
|
+
|
|
2026
2358
|
if (event.type === 'turn.done') {
|
|
2027
2359
|
if (event.promptId !== undefined && this.submission !== null && this.submission.input.promptId !== event.promptId) {
|
|
2028
2360
|
return;
|
|
2029
2361
|
}
|
|
2362
|
+
|
|
2030
2363
|
if (event.promptId !== undefined) {
|
|
2031
2364
|
this.staleResults = 0;
|
|
2032
2365
|
}
|
|
2366
|
+
|
|
2033
2367
|
if (event.replaceProcess === true) {
|
|
2034
2368
|
this.replaceProcess = true;
|
|
2035
2369
|
}
|
|
2370
|
+
|
|
2036
2371
|
const promptId = event.promptId ?? this.submission?.input.promptId;
|
|
2372
|
+
|
|
2037
2373
|
if (promptId !== undefined) {
|
|
2038
2374
|
this.acceptPrompt(promptId);
|
|
2039
2375
|
}
|
|
2040
2376
|
}
|
|
2377
|
+
|
|
2041
2378
|
const openTurnId = this.thread.info.activeTurnId;
|
|
2379
|
+
|
|
2042
2380
|
if ((event.type === 'turn.done' && event.state !== 'done') || ((event.type === 'failed' || event.type === 'exit') && openTurnId !== null)) {
|
|
2043
2381
|
this.pauseQueue(true);
|
|
2044
2382
|
}
|
|
2383
|
+
|
|
2045
2384
|
// Settled as failed by the exit below; the transcript may still show that they finished first.
|
|
2046
2385
|
const orphaned = event.type === 'exit' && this.info.provider === 'claude' ? this.runningBackgroundRows() : [];
|
|
2386
|
+
|
|
2047
2387
|
if (event.type === 'turn.done' && event.state === 'aborted') {
|
|
2048
2388
|
// A request of work beside the turn outlives the interrupt; turned down, that work goes on instead of waiting forever.
|
|
2049
2389
|
for (const item of this.thread.pending()) {
|
|
2050
2390
|
this.backend?.declineRequest?.(item.requestId, 'The user stopped the turn');
|
|
2051
2391
|
}
|
|
2052
2392
|
}
|
|
2393
|
+
|
|
2053
2394
|
const hadNativeWork = (event.type === 'task.done' || event.type === 'tool.done' || event.type === 'background.ended') && this.hasNativeWork();
|
|
2054
2395
|
const projected = this.projector.project(generation, event);
|
|
2396
|
+
|
|
2055
2397
|
if (event.type === 'turn.done' || (event.type === 'failed' && event.processAlive !== true)) {
|
|
2056
2398
|
this.awaitingBackgroundReport = false;
|
|
2057
2399
|
} else if (event.type !== 'exit' && event.type !== 'failed' && hadNativeWork && !this.hasNativeWork() && reportsOnBackgroundWork(this.info.provider)) {
|
|
2058
2400
|
this.awaitingBackgroundReport = true;
|
|
2059
2401
|
}
|
|
2402
|
+
|
|
2060
2403
|
this.emit(projected);
|
|
2061
2404
|
this.trackSubagentOwner(generation, event);
|
|
2062
2405
|
this.watchWorkflows(generation, event);
|
|
2406
|
+
|
|
2063
2407
|
if (isMainAgentOutput(event)) {
|
|
2064
2408
|
this.spokenTurnId = this.thread.info.activeTurnId;
|
|
2065
2409
|
}
|
|
2410
|
+
|
|
2066
2411
|
if (orphaned.length > 0) {
|
|
2067
2412
|
void this.settleFromTranscripts(orphaned.flatMap((row) => this.thread.get(row.id) ?? []).filter((item) => item.kind === 'subagent'));
|
|
2068
2413
|
}
|
|
2414
|
+
|
|
2069
2415
|
const activeTurnId = this.thread.info.activeTurnId;
|
|
2416
|
+
|
|
2070
2417
|
if (openTurnId === null && activeTurnId !== null) {
|
|
2071
2418
|
// The CLI opened this turn itself; it takes a checkpoint like any other, so the card can
|
|
2072
2419
|
// show what the agent changed while nobody was watching.
|
|
@@ -2074,20 +2421,26 @@ export class ChatSession {
|
|
|
2074
2421
|
void this.checkpoint(activeTurnId);
|
|
2075
2422
|
this.options.persist();
|
|
2076
2423
|
}
|
|
2424
|
+
|
|
2077
2425
|
if (event.type === 'turn.done' || event.type === 'exit' || event.type === 'failed') {
|
|
2078
2426
|
this.options.persist();
|
|
2079
2427
|
}
|
|
2428
|
+
|
|
2080
2429
|
if (event.type === 'permissions' || event.type === 'session') {
|
|
2081
2430
|
this.options.persist();
|
|
2082
2431
|
}
|
|
2432
|
+
|
|
2083
2433
|
if (openTurnId !== null && activeTurnId === null) {
|
|
2084
2434
|
this.settleCheckpoint(openTurnId);
|
|
2085
2435
|
}
|
|
2436
|
+
|
|
2086
2437
|
if (event.type === 'session' && typeof event.title === 'string') {
|
|
2087
2438
|
this.applyTitle(event.title);
|
|
2088
2439
|
}
|
|
2440
|
+
|
|
2089
2441
|
if (event.type === 'session' && this.thread.info.agentSessionId !== null) {
|
|
2090
2442
|
this.refreshTitle();
|
|
2443
|
+
|
|
2091
2444
|
if (this.titleTimer === null && this.thread.info.suggestedTitle === undefined) {
|
|
2092
2445
|
this.titleTimer = setTimeout(() => {
|
|
2093
2446
|
this.titleTimer = null;
|
|
@@ -2095,15 +2448,19 @@ export class ChatSession {
|
|
|
2095
2448
|
}, TITLE_RECHECK_MS);
|
|
2096
2449
|
}
|
|
2097
2450
|
}
|
|
2451
|
+
|
|
2098
2452
|
// The turn is over and the CLI is still there, so whatever waited behind it can go out now.
|
|
2099
2453
|
if (event.type === 'turn.done') {
|
|
2100
2454
|
this.refreshTitle();
|
|
2455
|
+
|
|
2101
2456
|
if (event.state === 'done') {
|
|
2102
2457
|
this.nameThread();
|
|
2103
2458
|
}
|
|
2459
|
+
|
|
2104
2460
|
if (event.limit !== undefined) {
|
|
2105
2461
|
this.oweResume(true);
|
|
2106
2462
|
}
|
|
2463
|
+
|
|
2107
2464
|
this.drainQueue(event.state === 'aborted' && this.sendAfterStop);
|
|
2108
2465
|
this.sendAfterStop = false;
|
|
2109
2466
|
} else if (
|
|
@@ -2123,9 +2480,11 @@ export class ChatSession {
|
|
|
2123
2480
|
|
|
2124
2481
|
private clearWorkflowWarnings(): void {
|
|
2125
2482
|
const clock = this.options.clock ?? systemClock;
|
|
2483
|
+
|
|
2126
2484
|
for (const timer of this.workflowWarnings.values()) {
|
|
2127
2485
|
clock.clearTimeout(timer);
|
|
2128
2486
|
}
|
|
2487
|
+
|
|
2129
2488
|
this.workflowWarnings.clear();
|
|
2130
2489
|
}
|
|
2131
2490
|
|
|
@@ -2133,15 +2492,20 @@ export class ChatSession {
|
|
|
2133
2492
|
if (event.type === 'tool.output' || event.type === 'tool.progress') {
|
|
2134
2493
|
// These come by the thousand, and only the call they name can be a workflow they move.
|
|
2135
2494
|
const item = this.projector.itemOf(generation, event.ref);
|
|
2495
|
+
|
|
2136
2496
|
if (item?.kind === 'tool' && item.name === 'Workflow') {
|
|
2137
2497
|
this.watchWorkflow(item, true);
|
|
2138
2498
|
}
|
|
2499
|
+
|
|
2139
2500
|
return;
|
|
2140
2501
|
}
|
|
2502
|
+
|
|
2141
2503
|
if (!['tool.started', 'tool.done', 'workflow.progress', 'task.started', 'task.done', 'exit', 'failed'].includes(event.type)) {
|
|
2142
2504
|
return;
|
|
2143
2505
|
}
|
|
2506
|
+
|
|
2144
2507
|
const ref = 'ref' in event ? event.ref : null;
|
|
2508
|
+
|
|
2145
2509
|
for (const item of this.thread.list()) {
|
|
2146
2510
|
if (item.kind === 'tool' && item.name === 'Workflow') {
|
|
2147
2511
|
this.watchWorkflow(item, ref === item.toolUseId && ['workflow.progress', 'tool.started', 'tool.done', 'task.started'].includes(event.type));
|
|
@@ -2152,15 +2516,18 @@ export class ChatSession {
|
|
|
2152
2516
|
private watchWorkflow(item: ChatToolItem, progress: boolean): void {
|
|
2153
2517
|
const clock = this.options.clock ?? systemClock;
|
|
2154
2518
|
const existing = this.workflowWarnings.get(item.id);
|
|
2519
|
+
|
|
2155
2520
|
if (item.state !== 'running' || progress) {
|
|
2156
2521
|
if (existing !== undefined) {
|
|
2157
2522
|
clock.clearTimeout(existing);
|
|
2158
2523
|
this.workflowWarnings.delete(item.id);
|
|
2159
2524
|
}
|
|
2160
2525
|
}
|
|
2526
|
+
|
|
2161
2527
|
if (item.state !== 'running' || (!progress && existing !== undefined) || (!progress && item.workflow?.stalledAt !== undefined)) {
|
|
2162
2528
|
return;
|
|
2163
2529
|
}
|
|
2530
|
+
|
|
2164
2531
|
if (progress) {
|
|
2165
2532
|
this.emit([
|
|
2166
2533
|
this.thread.upsert({
|
|
@@ -2169,14 +2536,17 @@ export class ChatSession {
|
|
|
2169
2536
|
})
|
|
2170
2537
|
]);
|
|
2171
2538
|
}
|
|
2539
|
+
|
|
2172
2540
|
this.workflowWarnings.set(
|
|
2173
2541
|
item.id,
|
|
2174
2542
|
clock.setTimeout(() => {
|
|
2175
2543
|
this.workflowWarnings.delete(item.id);
|
|
2176
2544
|
const current = this.thread.get(item.id);
|
|
2545
|
+
|
|
2177
2546
|
if (this.frozen || current?.kind !== 'tool' || current.state !== 'running') {
|
|
2178
2547
|
return;
|
|
2179
2548
|
}
|
|
2549
|
+
|
|
2180
2550
|
this.emit([this.thread.upsert({ ...current, workflow: { name: null, phases: [], agents: [], ...current.workflow, stalledAt: clock.now() } })]);
|
|
2181
2551
|
this.options.persist();
|
|
2182
2552
|
}, 5 * 60_000)
|
|
@@ -2186,15 +2556,18 @@ export class ChatSession {
|
|
|
2186
2556
|
/* Whether the CLI that reports was started on a pick standing for the same window as the chat's own. */
|
|
2187
2557
|
private reportsCurrentWindow(): boolean {
|
|
2188
2558
|
const catalog = this.options.provider.catalog;
|
|
2559
|
+
|
|
2189
2560
|
return this.launchedSelection === null || catalog.contextWindowFor(this.launchedSelection) === catalog.contextWindowFor(this.thread.info.selection);
|
|
2190
2561
|
}
|
|
2191
2562
|
|
|
2192
2563
|
/* Rides on the info patch: a client that has the chat open hears it, and a reload finds it stored. */
|
|
2193
2564
|
private refreshTitle(): void {
|
|
2194
2565
|
const agentSessionId = this.thread.info.agentSessionId;
|
|
2566
|
+
|
|
2195
2567
|
if (!this.options.readTitle || agentSessionId === null) {
|
|
2196
2568
|
return;
|
|
2197
2569
|
}
|
|
2570
|
+
|
|
2198
2571
|
void this.options
|
|
2199
2572
|
.readTitle(agentSessionId)
|
|
2200
2573
|
.then((title) => {
|
|
@@ -2202,6 +2575,7 @@ export class ChatSession {
|
|
|
2202
2575
|
if (title === null || title === this.thread.info.suggestedTitle || this.thread.info.agentSessionId !== agentSessionId) {
|
|
2203
2576
|
return;
|
|
2204
2577
|
}
|
|
2578
|
+
|
|
2205
2579
|
this.emit([this.thread.patchInfo({ suggestedTitle: title })]);
|
|
2206
2580
|
this.options.persistSoon();
|
|
2207
2581
|
})
|
|
@@ -2211,9 +2585,11 @@ export class ChatSession {
|
|
|
2211
2585
|
private applyTitle(raw: string): void {
|
|
2212
2586
|
// Every info a client reads carries the title, so one it would refuse would leave the chat stuck there.
|
|
2213
2587
|
const title = cleanTitle(raw);
|
|
2588
|
+
|
|
2214
2589
|
if (title === null || title === this.thread.info.suggestedTitle) {
|
|
2215
2590
|
return;
|
|
2216
2591
|
}
|
|
2592
|
+
|
|
2217
2593
|
this.emit([this.thread.patchInfo({ suggestedTitle: title })]);
|
|
2218
2594
|
this.options.persistSoon();
|
|
2219
2595
|
}
|
|
@@ -2225,28 +2601,36 @@ export class ChatSession {
|
|
|
2225
2601
|
*/
|
|
2226
2602
|
private nameThread(): void {
|
|
2227
2603
|
const name = this.options.nameThread;
|
|
2604
|
+
|
|
2228
2605
|
if (!name || this.naming || this.thread.info.suggestedTitle !== undefined) {
|
|
2229
2606
|
return;
|
|
2230
2607
|
}
|
|
2608
|
+
|
|
2231
2609
|
const items = this.thread.list();
|
|
2232
2610
|
const done = items.filter((item) => item.kind === 'turn' && item.state === 'done');
|
|
2233
2611
|
const prompt = items.find((item) => item.kind === 'user');
|
|
2612
|
+
|
|
2234
2613
|
if (done.length !== 1 || prompt?.kind !== 'user' || prompt.text.trim() === '') {
|
|
2235
2614
|
return;
|
|
2236
2615
|
}
|
|
2616
|
+
|
|
2237
2617
|
this.naming = true;
|
|
2238
2618
|
const turnId = done[0]!.id;
|
|
2619
|
+
|
|
2239
2620
|
const answer = items
|
|
2240
2621
|
.map((item) => (item.kind === 'assistant' && item.turnId === turnId ? readableAssistantText(item) : ''))
|
|
2241
2622
|
.filter((text) => text !== '')
|
|
2242
2623
|
.join('\n\n');
|
|
2624
|
+
|
|
2243
2625
|
const agentSessionId = this.thread.info.agentSessionId;
|
|
2626
|
+
|
|
2244
2627
|
void name({ cwd: this.thread.info.cwd, prompt: prompt.text, answer })
|
|
2245
2628
|
.then((title) => {
|
|
2246
2629
|
// A clear in between started another conversation, and a name that came meanwhile is the CLI's own.
|
|
2247
2630
|
if (title === null || this.thread.info.agentSessionId !== agentSessionId || this.thread.info.suggestedTitle !== undefined) {
|
|
2248
2631
|
return;
|
|
2249
2632
|
}
|
|
2633
|
+
|
|
2250
2634
|
this.applyTitle(title);
|
|
2251
2635
|
this.backend?.setTitle?.(title);
|
|
2252
2636
|
})
|
|
@@ -2260,17 +2644,23 @@ export class ChatSession {
|
|
|
2260
2644
|
/* Done for every row whose transcript shows the end, unless something else settled the row while it was read. */
|
|
2261
2645
|
private async settleFromTranscripts(rows: ChatSubagentItem[]): Promise<void> {
|
|
2262
2646
|
const lookup = this.options.subagentSettlement;
|
|
2647
|
+
|
|
2263
2648
|
if (!lookup || rows.length === 0) {
|
|
2264
2649
|
return;
|
|
2265
2650
|
}
|
|
2651
|
+
|
|
2266
2652
|
const settled: ChatSubagentItem[] = [];
|
|
2653
|
+
|
|
2267
2654
|
for (const row of rows) {
|
|
2268
2655
|
const settlement = await lookup(row.toolUseId);
|
|
2269
2656
|
const current = this.thread.get(row.id);
|
|
2657
|
+
|
|
2270
2658
|
if (settlement === null || current?.kind !== 'subagent' || current.status !== row.status || current.finishedAt !== row.finishedAt) {
|
|
2271
2659
|
continue;
|
|
2272
2660
|
}
|
|
2661
|
+
|
|
2273
2662
|
this.orphans.delete(row.toolUseId);
|
|
2663
|
+
|
|
2274
2664
|
settled.push({
|
|
2275
2665
|
...current,
|
|
2276
2666
|
status: 'done',
|
|
@@ -2278,6 +2668,7 @@ export class ChatSession {
|
|
|
2278
2668
|
result: settlement.report ?? current.result
|
|
2279
2669
|
});
|
|
2280
2670
|
}
|
|
2671
|
+
|
|
2281
2672
|
this.settleRows(settled);
|
|
2282
2673
|
}
|
|
2283
2674
|
|
|
@@ -2285,12 +2676,14 @@ export class ChatSession {
|
|
|
2285
2676
|
if (this.frozen || rows.length === 0) {
|
|
2286
2677
|
return;
|
|
2287
2678
|
}
|
|
2679
|
+
|
|
2288
2680
|
this.emit(rows.map((row) => this.thread.upsert(row)));
|
|
2289
2681
|
this.options.persist();
|
|
2290
2682
|
}
|
|
2291
2683
|
|
|
2292
2684
|
private emit(events: ChatEvent[]): void {
|
|
2293
2685
|
const delegating = this.delegatingChange(events);
|
|
2686
|
+
|
|
2294
2687
|
for (const event of delegating === null ? events : [...events, delegating]) {
|
|
2295
2688
|
this.uiReplies.pass(event);
|
|
2296
2689
|
}
|
|
@@ -2301,10 +2694,13 @@ export class ChatSession {
|
|
|
2301
2694
|
if (!events.some((event) => event.type === 'item' && isBackgroundWork(event.item))) {
|
|
2302
2695
|
return null;
|
|
2303
2696
|
}
|
|
2697
|
+
|
|
2304
2698
|
const delegating = runningInBackground(this.thread.list()).length > 0;
|
|
2699
|
+
|
|
2305
2700
|
if (delegating === (this.thread.info.delegating ?? false)) {
|
|
2306
2701
|
return null;
|
|
2307
2702
|
}
|
|
2703
|
+
|
|
2308
2704
|
return this.thread.patchInfo({ delegating: delegating || undefined });
|
|
2309
2705
|
}
|
|
2310
2706
|
}
|
|
@@ -2320,18 +2716,23 @@ function settleStoredItem(item: ChatItem, resumeTurnId: string | null): ChatItem
|
|
|
2320
2716
|
if (item.kind === 'assistant' && item.streaming) {
|
|
2321
2717
|
return { ...item, streaming: false };
|
|
2322
2718
|
}
|
|
2719
|
+
|
|
2323
2720
|
if (item.kind === 'approval' && item.decision === 'pending') {
|
|
2324
2721
|
return { ...item, decision: 'cancelled' };
|
|
2325
2722
|
}
|
|
2723
|
+
|
|
2326
2724
|
if (item.kind === 'question' && item.state === 'pending') {
|
|
2327
2725
|
return { ...item, state: 'cancelled' };
|
|
2328
2726
|
}
|
|
2727
|
+
|
|
2329
2728
|
if (item.kind === 'tool' && item.state === 'running') {
|
|
2330
2729
|
return { ...item, state: 'error' };
|
|
2331
2730
|
}
|
|
2731
|
+
|
|
2332
2732
|
// Aborted rather than error: the turn did not fail, the machine went down under it.
|
|
2333
2733
|
if (item.kind === 'turn' && item.state === 'running' && item.id !== resumeTurnId) {
|
|
2334
2734
|
return { ...item, state: 'aborted', endedAt: item.endedAt ?? item.createdAt };
|
|
2335
2735
|
}
|
|
2736
|
+
|
|
2336
2737
|
return item;
|
|
2337
2738
|
}
|