@adecore/agents 0.39.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.js +0 -2
- package/dist/chat/chat-process.d.ts +1 -0
- package/dist/chat/chat-process.js +1 -1
- package/dist/chat/chat-session.js +6 -8
- package/dist/chat/claude-protocol.js +1 -6
- package/dist/chat/claude-transcript.js +1 -3
- package/dist/chat/codex-protocol.js +1 -6
- package/dist/chat/projector.js +1 -3
- package/dist/chat/request-summary.js +1 -3
- package/dist/coded-error.js +1 -5
- package/dist/context/argv.js +1 -2
- package/dist/context/verb.js +1 -3
- package/dist/error-text.d.ts +1 -0
- package/dist/error-text.js +4 -0
- package/dist/fields.d.ts +3 -0
- package/dist/fields.js +12 -0
- package/dist/fs.d.ts +1 -0
- package/dist/fs.js +8 -4
- package/dist/lineage.js +11 -5
- package/dist/messages/notice-store.js +3 -4
- package/dist/providers/accounts/acp-account.js +3 -20
- package/dist/providers/accounts/service.d.ts +1 -2
- package/dist/providers/accounts/service.js +5 -9
- package/dist/providers/accounts/status.js +1 -3
- package/dist/providers/acp-agent-state.js +1 -3
- package/dist/providers/claude.js +0 -1
- package/dist/providers/codex.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 +208 -7
- package/src/chat/chat-log.ts +19 -0
- package/src/chat/chat-process.ts +52 -1
- package/src/chat/chat-session.ts +401 -10
- package/src/chat/chat-store.ts +50 -0
- package/src/chat/chat-title.ts +9 -0
- package/src/chat/claude-backend.ts +50 -0
- package/src/chat/claude-protocol.ts +155 -8
- package/src/chat/claude-title.ts +10 -0
- package/src/chat/claude-transcript.ts +47 -4
- package/src/chat/codex-backend.ts +131 -0
- package/src/chat/codex-protocol.ts +180 -8
- package/src/chat/codex-thread.ts +23 -0
- package/src/chat/composer-preferences.ts +6 -0
- package/src/chat/context-breakdown.ts +11 -0
- package/src/chat/delta-coalescer.ts +8 -0
- package/src/chat/external-sessions.ts +28 -0
- package/src/chat/generated-image.ts +25 -0
- package/src/chat/index.ts +5 -0
- package/src/chat/input.ts +13 -0
- package/src/chat/json-rpc-transport.ts +28 -0
- package/src/chat/limit-resume.ts +13 -0
- package/src/chat/projector.ts +160 -4
- package/src/chat/readable-text.ts +3 -0
- package/src/chat/request-summary.ts +18 -4
- package/src/chat/subagent-projection.ts +3 -0
- package/src/chat/subagent-reader.ts +76 -0
- package/src/chat/subagent-settlement.ts +24 -0
- package/src/chat/thread-search.ts +13 -0
- package/src/chat/thread.ts +31 -0
- package/src/chat/ui-queries.ts +122 -0
- package/src/chat/ui-replies.ts +34 -0
- package/src/chat/visual-store.ts +49 -0
- package/src/chat/wake-chat.ts +3 -0
- package/src/client-sinks.ts +2 -0
- package/src/coded-error.ts +1 -5
- package/src/context/argv.ts +18 -2
- package/src/context/index.ts +1 -0
- package/src/context/refusal.ts +3 -0
- package/src/context/verb.ts +32 -4
- package/src/coordination/index.ts +10 -0
- package/src/error-text.ts +7 -0
- package/src/fields.ts +14 -0
- package/src/fs.ts +20 -4
- package/src/host/agent-host.ts +20 -0
- package/src/host/environment.ts +6 -0
- package/src/host/handlers.ts +18 -0
- package/src/host/memory-port.ts +4 -0
- package/src/host/wiring.ts +10 -0
- package/src/index.ts +1 -0
- package/src/lineage.ts +24 -5
- package/src/messages/deliver-message.ts +6 -0
- package/src/messages/deliver-notice.ts +13 -0
- package/src/messages/notice-store.ts +16 -4
- package/src/modes.ts +1 -0
- package/src/outbox/outbox-worker.ts +31 -0
- package/src/outbox/outbox.ts +5 -0
- package/src/providers/accounts/accounts.ts +36 -0
- package/src/providers/accounts/acp-account.ts +22 -27
- package/src/providers/accounts/launch.ts +4 -0
- package/src/providers/accounts/service.ts +110 -12
- package/src/providers/accounts/shadow-home.ts +10 -0
- package/src/providers/accounts/status.ts +13 -4
- package/src/providers/accounts/store.ts +25 -0
- package/src/providers/accounts/variables.ts +11 -1
- package/src/providers/acp-agent-state.ts +24 -3
- package/src/providers/acp-provider.ts +5 -0
- package/src/providers/acp-usage-log.ts +2 -0
- package/src/providers/catalog.ts +17 -0
- package/src/providers/claude.ts +10 -1
- package/src/providers/codex-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;
|
|
@@ -1094,10 +1245,12 @@ export class ChatSession {
|
|
|
1094
1245
|
if (this.frozen || this.relocating || this.thread.info.activeTurnId !== null || this.replacementWaits()) {
|
|
1095
1246
|
return null;
|
|
1096
1247
|
}
|
|
1248
|
+
|
|
1097
1249
|
this.turnOpened();
|
|
1098
1250
|
const { preamble, note } = this.contextNote(wake.text);
|
|
1099
1251
|
const turnId = newId('turn');
|
|
1100
1252
|
const now = Date.now();
|
|
1253
|
+
|
|
1101
1254
|
this.emit([
|
|
1102
1255
|
this.thread.upsert({
|
|
1103
1256
|
id: turnId,
|
|
@@ -1120,9 +1273,11 @@ export class ChatSession {
|
|
|
1120
1273
|
...(note === null ? [] : [this.thread.upsert({ id: newId('note'), kind: 'note', createdAt: now, turnId, level: 'info', text: note })]),
|
|
1121
1274
|
this.thread.patchInfo({ status: this.thread.statusFor(turnId), activeTurnId: turnId })
|
|
1122
1275
|
]);
|
|
1276
|
+
|
|
1123
1277
|
this.options.persist();
|
|
1124
1278
|
this.turnReady = this.checkpoint(turnId);
|
|
1125
1279
|
this.submit(turnId, { text: wake.text, preamble, attachments: [], mentions: wake.mentions ?? [], skills: wake.skills ?? [] });
|
|
1280
|
+
|
|
1126
1281
|
return turnId;
|
|
1127
1282
|
}
|
|
1128
1283
|
|
|
@@ -1143,20 +1298,27 @@ export class ChatSession {
|
|
|
1143
1298
|
*/
|
|
1144
1299
|
takeUpAfterLimit(turnId: string): 'opened' | 'wait' | 'lapsed' {
|
|
1145
1300
|
const turn = limitedTurn(this.thread.list());
|
|
1301
|
+
|
|
1146
1302
|
if (turn === null || turn.id !== turnId || !this.resumeAllowed()) {
|
|
1147
1303
|
this.lapseResume();
|
|
1304
|
+
|
|
1148
1305
|
return 'lapsed';
|
|
1149
1306
|
}
|
|
1307
|
+
|
|
1150
1308
|
const wake = limitResumeWake(turn.limit.kind);
|
|
1309
|
+
|
|
1151
1310
|
const opened = this.wake({
|
|
1152
1311
|
...wake,
|
|
1153
1312
|
taskIds: turn.taskIds ?? [],
|
|
1154
1313
|
...(turn.messageFrom === undefined ? {} : { messageFrom: turn.messageFrom })
|
|
1155
1314
|
});
|
|
1315
|
+
|
|
1156
1316
|
if (opened === null) {
|
|
1157
1317
|
return 'wait';
|
|
1158
1318
|
}
|
|
1319
|
+
|
|
1159
1320
|
this.pauseQueue(false);
|
|
1321
|
+
|
|
1160
1322
|
return 'opened';
|
|
1161
1323
|
}
|
|
1162
1324
|
|
|
@@ -1182,17 +1344,23 @@ export class ChatSession {
|
|
|
1182
1344
|
private oweResume(fresh: boolean): void {
|
|
1183
1345
|
const hooks = this.options.limitResume;
|
|
1184
1346
|
const turn = limitedTurn(this.thread.list());
|
|
1347
|
+
|
|
1185
1348
|
if (!hooks || turn === null || this.thread.info.activeTurnId !== null || this.frozen || !this.resumeAllowed() || this.wentOn(turn)) {
|
|
1186
1349
|
return;
|
|
1187
1350
|
}
|
|
1351
|
+
|
|
1188
1352
|
const now = hooks.now();
|
|
1353
|
+
|
|
1189
1354
|
if (!fresh && (turn.limit.kind !== 'usage' || (turn.limit.resetsAt ?? 0) <= now)) {
|
|
1190
1355
|
return;
|
|
1191
1356
|
}
|
|
1357
|
+
|
|
1192
1358
|
const at = limitResumeAt(this.thread.list(), turn, now);
|
|
1359
|
+
|
|
1193
1360
|
if (at === null || at === this.thread.info.resumeAt) {
|
|
1194
1361
|
return;
|
|
1195
1362
|
}
|
|
1363
|
+
|
|
1196
1364
|
this.emit([this.thread.patchInfo({ resumeAt: at })]);
|
|
1197
1365
|
this.options.persist();
|
|
1198
1366
|
void hooks.owe(turn.id, at).catch((e: unknown) => console.error(`Owing a resume of chat ${this.id} failed:`, errorText(e)));
|
|
@@ -1205,15 +1373,20 @@ export class ChatSession {
|
|
|
1205
1373
|
settleOwedResume(): void {
|
|
1206
1374
|
const hooks = this.options.limitResume;
|
|
1207
1375
|
const at = this.thread.info.resumeAt;
|
|
1376
|
+
|
|
1208
1377
|
if (at === undefined || hooks?.owed() === true) {
|
|
1209
1378
|
return;
|
|
1210
1379
|
}
|
|
1380
|
+
|
|
1211
1381
|
const turn = limitedTurn(this.thread.list());
|
|
1382
|
+
|
|
1212
1383
|
if (!hooks || turn === null || this.thread.info.activeTurnId !== null || !this.resumeAllowed()) {
|
|
1213
1384
|
this.emit([this.thread.patchInfo({ resumeAt: undefined })]);
|
|
1214
1385
|
this.options.persist();
|
|
1386
|
+
|
|
1215
1387
|
return;
|
|
1216
1388
|
}
|
|
1389
|
+
|
|
1217
1390
|
void hooks.owe(turn.id, at).catch((e: unknown) => console.error(`Owing a resume of chat ${this.id} failed:`, errorText(e)));
|
|
1218
1391
|
}
|
|
1219
1392
|
|
|
@@ -1222,6 +1395,7 @@ export class ChatSession {
|
|
|
1222
1395
|
if (this.thread.info.limit !== undefined) {
|
|
1223
1396
|
this.emit([this.thread.patchInfo({ limit: undefined })]);
|
|
1224
1397
|
}
|
|
1398
|
+
|
|
1225
1399
|
this.lapseResume();
|
|
1226
1400
|
}
|
|
1227
1401
|
|
|
@@ -1232,9 +1406,11 @@ export class ChatSession {
|
|
|
1232
1406
|
continuedElsewhere(note: string): void {
|
|
1233
1407
|
this.lapseResume();
|
|
1234
1408
|
const turn = limitedTurn(this.thread.list());
|
|
1409
|
+
|
|
1235
1410
|
if (turn === null || this.wentOn(turn)) {
|
|
1236
1411
|
return;
|
|
1237
1412
|
}
|
|
1413
|
+
|
|
1238
1414
|
this.emit([this.thread.upsert({ id: continuedNoteId(turn.id), kind: 'note', createdAt: Date.now(), turnId: turn.id, level: 'info', text: note })]);
|
|
1239
1415
|
this.options.persist();
|
|
1240
1416
|
}
|
|
@@ -1242,9 +1418,11 @@ export class ChatSession {
|
|
|
1242
1418
|
/* Drops the resume owed after a limit, the time the thread shows for it included. */
|
|
1243
1419
|
lapseResume(): void {
|
|
1244
1420
|
const hooks = this.options.limitResume;
|
|
1421
|
+
|
|
1245
1422
|
if (this.thread.info.resumeAt === undefined || !hooks) {
|
|
1246
1423
|
return;
|
|
1247
1424
|
}
|
|
1425
|
+
|
|
1248
1426
|
this.emit([this.thread.patchInfo({ resumeAt: undefined })]);
|
|
1249
1427
|
this.options.persist();
|
|
1250
1428
|
void hooks.lapse().catch((e: unknown) => console.error(`Dropping the resume of chat ${this.id} failed:`, errorText(e)));
|
|
@@ -1259,7 +1437,9 @@ export class ChatSession {
|
|
|
1259
1437
|
if (this.thread.get(delivery.noteId) !== undefined) {
|
|
1260
1438
|
return false;
|
|
1261
1439
|
}
|
|
1440
|
+
|
|
1262
1441
|
this.pendingPreambles = [...this.pendingPreambles, delivery.preamble];
|
|
1442
|
+
|
|
1263
1443
|
this.emit([
|
|
1264
1444
|
this.thread.upsert({
|
|
1265
1445
|
id: delivery.noteId,
|
|
@@ -1271,16 +1451,20 @@ export class ChatSession {
|
|
|
1271
1451
|
...(delivery.from === undefined ? {} : { from: delivery.from })
|
|
1272
1452
|
})
|
|
1273
1453
|
]);
|
|
1454
|
+
|
|
1274
1455
|
this.options.persist();
|
|
1456
|
+
|
|
1275
1457
|
return true;
|
|
1276
1458
|
}
|
|
1277
1459
|
|
|
1278
1460
|
/* Writes items a host draws in the thread itself (a row for work it delegated), leaving the ones that did not change alone. */
|
|
1279
1461
|
upsertItems(items: readonly ChatItem[]): void {
|
|
1280
1462
|
const changed = items.filter((item) => JSON.stringify(this.thread.get(item.id)) !== JSON.stringify(item));
|
|
1463
|
+
|
|
1281
1464
|
if (changed.length === 0) {
|
|
1282
1465
|
return;
|
|
1283
1466
|
}
|
|
1467
|
+
|
|
1284
1468
|
this.emit(changed.map((item) => this.thread.upsert(item)));
|
|
1285
1469
|
this.options.persistSoon();
|
|
1286
1470
|
}
|
|
@@ -1310,6 +1494,7 @@ export class ChatSession {
|
|
|
1310
1494
|
const now = Date.now();
|
|
1311
1495
|
const turnId = this.thread.info.activeTurnId;
|
|
1312
1496
|
const events: ChatEvent[] = [];
|
|
1497
|
+
|
|
1313
1498
|
for (const item of this.thread.list()) {
|
|
1314
1499
|
const settled =
|
|
1315
1500
|
item.kind === 'turn' && item.state === 'running'
|
|
@@ -1317,13 +1502,16 @@ export class ChatSession {
|
|
|
1317
1502
|
: item.kind === 'subagent' && item.origin !== HOST_TASK_ORIGIN && item.status === 'running'
|
|
1318
1503
|
? { ...item, status: 'failed' as const, finishedAt: now }
|
|
1319
1504
|
: settleStoredItem(item, null);
|
|
1505
|
+
|
|
1320
1506
|
if (settled !== item) {
|
|
1321
1507
|
events.push(this.thread.upsert(settled));
|
|
1322
1508
|
}
|
|
1323
1509
|
}
|
|
1510
|
+
|
|
1324
1511
|
if (turnId !== null) {
|
|
1325
1512
|
events.push(this.thread.upsert({ id: newId(`${ENDED_NOTE_PREFIX}note`), kind: 'note', createdAt: now, turnId, level: 'warning', text: reason }));
|
|
1326
1513
|
}
|
|
1514
|
+
|
|
1327
1515
|
events.push(
|
|
1328
1516
|
this.thread.patchInfo({
|
|
1329
1517
|
running: false,
|
|
@@ -1335,6 +1523,7 @@ export class ChatSession {
|
|
|
1335
1523
|
...(this.thread.info.background?.length ? { background: [] } : {})
|
|
1336
1524
|
})
|
|
1337
1525
|
);
|
|
1526
|
+
|
|
1338
1527
|
this.emit(events);
|
|
1339
1528
|
this.options.persist();
|
|
1340
1529
|
}
|
|
@@ -1348,6 +1537,7 @@ export class ChatSession {
|
|
|
1348
1537
|
if (this.thread.info.activeTurnId !== null || this.processInUse()) {
|
|
1349
1538
|
throw new ChatError('chat-busy', `Chat ${this.id} is still working; stop it before restarting its agent session`);
|
|
1350
1539
|
}
|
|
1540
|
+
|
|
1351
1541
|
return this.dropProcess();
|
|
1352
1542
|
}
|
|
1353
1543
|
|
|
@@ -1365,6 +1555,7 @@ export class ChatSession {
|
|
|
1365
1555
|
// What the init frame of the process that goes announced; a skill added since would be filtered out by it.
|
|
1366
1556
|
this.emit([this.thread.patchInfo({ running: false, effectiveRuntimeMode: undefined, permissionMode: undefined, skills: undefined })]);
|
|
1367
1557
|
this.options.persist();
|
|
1558
|
+
|
|
1368
1559
|
return backend?.dispose() ?? Promise.resolve();
|
|
1369
1560
|
}
|
|
1370
1561
|
|
|
@@ -1382,50 +1573,63 @@ export class ChatSession {
|
|
|
1382
1573
|
this.uiReplies.clear();
|
|
1383
1574
|
this.clearStartWarning();
|
|
1384
1575
|
this.clearWorkflowWarnings();
|
|
1576
|
+
|
|
1385
1577
|
if (this.titleTimer !== null) {
|
|
1386
1578
|
clearTimeout(this.titleTimer);
|
|
1387
1579
|
this.titleTimer = null;
|
|
1388
1580
|
}
|
|
1581
|
+
|
|
1389
1582
|
const ended = this.backend?.dispose() ?? Promise.resolve();
|
|
1390
1583
|
this.backend = null;
|
|
1391
1584
|
this.starting = null;
|
|
1585
|
+
|
|
1392
1586
|
return Promise.all([ended, ...this.checkpointWork]).then(() => undefined);
|
|
1393
1587
|
}
|
|
1394
1588
|
|
|
1395
1589
|
/* What a turn changed against its checkpoint: the stored answer, or one taken now while it runs. */
|
|
1396
1590
|
async turnDiff(turnId: string): Promise<ChatCheckpointDiff | null> {
|
|
1397
1591
|
const turn = this.thread.get(turnId);
|
|
1592
|
+
|
|
1398
1593
|
if (turn?.kind !== 'turn') {
|
|
1399
1594
|
throw new ChatError('request-not-found', `No turn ${turnId} in chat ${this.id}`);
|
|
1400
1595
|
}
|
|
1596
|
+
|
|
1401
1597
|
if (turn.checkpointDiff) {
|
|
1402
1598
|
return turn.checkpointDiff;
|
|
1403
1599
|
}
|
|
1600
|
+
|
|
1404
1601
|
if (turn.checkpoint === undefined || !this.options.checkpoints) {
|
|
1405
1602
|
return null;
|
|
1406
1603
|
}
|
|
1604
|
+
|
|
1407
1605
|
return await this.options.checkpoints.diff(this.thread.info.cwd, turn.checkpoint);
|
|
1408
1606
|
}
|
|
1409
1607
|
|
|
1410
1608
|
/* A page of a thread the CLI keeps, through the process that runs now; null when none runs that can answer. */
|
|
1411
1609
|
listThreadItems(params: { threadId: string; cursor?: string; limit: number; sortDirection: 'asc' | 'desc' }): Promise<unknown> | null {
|
|
1412
1610
|
const backend = this.backend;
|
|
1611
|
+
|
|
1413
1612
|
return backend?.running === true && backend.listThreadItems ? backend.listThreadItems(params) : null;
|
|
1414
1613
|
}
|
|
1415
1614
|
|
|
1416
1615
|
/* A completed transcript survives the process; unfinished work cannot send a result after a restart. */
|
|
1417
1616
|
async settleOrphanedSubagents(): Promise<void> {
|
|
1418
1617
|
const rows = this.runningBackgroundRows();
|
|
1618
|
+
|
|
1419
1619
|
if (this.info.provider === 'claude') {
|
|
1420
1620
|
for (const row of rows) {
|
|
1421
1621
|
this.orphans.add(row.toolUseId);
|
|
1422
1622
|
}
|
|
1623
|
+
|
|
1423
1624
|
await this.settleFromTranscripts(rows);
|
|
1424
1625
|
}
|
|
1626
|
+
|
|
1425
1627
|
const lost = this.runningBackgroundRows();
|
|
1628
|
+
|
|
1426
1629
|
for (const row of lost) {
|
|
1427
1630
|
this.recordLostBackground(row);
|
|
1428
1631
|
}
|
|
1632
|
+
|
|
1429
1633
|
this.settleRows(lost.map((row) => ({ ...row, status: 'failed', finishedAt: Date.now() })));
|
|
1430
1634
|
}
|
|
1431
1635
|
|
|
@@ -1444,15 +1648,20 @@ export class ChatSession {
|
|
|
1444
1648
|
const row = this.thread.find('subagent', (item) => item.toolUseId === toolUseId);
|
|
1445
1649
|
const agentId = row?.native?.agentId;
|
|
1446
1650
|
const owned = this.subagentOwners.get(toolUseId) === this.generation;
|
|
1651
|
+
|
|
1447
1652
|
if (agentId !== undefined && owned && this.backend?.stopTask) {
|
|
1448
1653
|
this.stopping.set(agentId, toolUseId);
|
|
1449
1654
|
this.backend.stopTask(agentId);
|
|
1655
|
+
|
|
1450
1656
|
return 'stopping';
|
|
1451
1657
|
}
|
|
1658
|
+
|
|
1452
1659
|
if (agentId !== undefined && !owned && this.thread.info.activeTurnId !== null) {
|
|
1453
1660
|
throw new ChatError('subagent-stale', 'This sub-agent belongs to a process that has ended');
|
|
1454
1661
|
}
|
|
1662
|
+
|
|
1455
1663
|
this.markSubagentStopped(toolUseId);
|
|
1664
|
+
|
|
1456
1665
|
return 'marked';
|
|
1457
1666
|
}
|
|
1458
1667
|
|
|
@@ -1461,6 +1670,7 @@ export class ChatSession {
|
|
|
1461
1670
|
this.subagentOwners.set(event.ref, generation);
|
|
1462
1671
|
} else if (event.type === 'task.done') {
|
|
1463
1672
|
this.noteStopped(event);
|
|
1673
|
+
|
|
1464
1674
|
if (event.ref !== null) {
|
|
1465
1675
|
this.subagentOwners.delete(event.ref);
|
|
1466
1676
|
}
|
|
@@ -1472,18 +1682,24 @@ export class ChatSession {
|
|
|
1472
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. */
|
|
1473
1683
|
private refuseStop(agentId: string, message: string): void {
|
|
1474
1684
|
const toolUseId = this.stopping.get(agentId);
|
|
1685
|
+
|
|
1475
1686
|
if (toolUseId === undefined) {
|
|
1476
1687
|
return;
|
|
1477
1688
|
}
|
|
1689
|
+
|
|
1478
1690
|
this.stopping.delete(agentId);
|
|
1479
1691
|
const row = this.thread.find('subagent', (item) => item.toolUseId === toolUseId);
|
|
1692
|
+
|
|
1480
1693
|
if (row?.status !== 'running') {
|
|
1481
1694
|
return;
|
|
1482
1695
|
}
|
|
1696
|
+
|
|
1483
1697
|
if (this.thread.info.activeTurnId === null) {
|
|
1484
1698
|
this.markSubagentStopped(toolUseId);
|
|
1699
|
+
|
|
1485
1700
|
return;
|
|
1486
1701
|
}
|
|
1702
|
+
|
|
1487
1703
|
this.emit([
|
|
1488
1704
|
this.thread.upsert({
|
|
1489
1705
|
id: `stop-refused-${toolUseId}-${Date.now()}`,
|
|
@@ -1494,25 +1710,33 @@ export class ChatSession {
|
|
|
1494
1710
|
text: `"${row.description || row.subagentType || 'Sub-agent'}" could not be stopped: ${message}`
|
|
1495
1711
|
})
|
|
1496
1712
|
]);
|
|
1713
|
+
|
|
1497
1714
|
this.options.persist();
|
|
1498
1715
|
}
|
|
1499
1716
|
|
|
1500
1717
|
/* The line under a subagent that `stop_task` ended, once the CLI reported it settled. */
|
|
1501
1718
|
private noteStopped(event: Extract<BackendEvent, { type: 'task.done' }>): void {
|
|
1502
1719
|
const stopped = [...this.stopping].find(([agentId, toolUseId]) => agentId === event.taskId || toolUseId === event.ref);
|
|
1720
|
+
|
|
1503
1721
|
if (stopped === undefined) {
|
|
1504
1722
|
return;
|
|
1505
1723
|
}
|
|
1724
|
+
|
|
1506
1725
|
this.stopping.delete(stopped[0]);
|
|
1726
|
+
|
|
1507
1727
|
// It finished on its own just before the stop arrived; the row already says so.
|
|
1508
1728
|
if (event.ok) {
|
|
1509
1729
|
return;
|
|
1510
1730
|
}
|
|
1731
|
+
|
|
1511
1732
|
const row = this.thread.find('subagent', (item) => item.toolUseId === stopped[1]);
|
|
1733
|
+
|
|
1512
1734
|
if (!row) {
|
|
1513
1735
|
return;
|
|
1514
1736
|
}
|
|
1737
|
+
|
|
1515
1738
|
const now = Date.now();
|
|
1739
|
+
|
|
1516
1740
|
this.emit([
|
|
1517
1741
|
...(row.status === 'running' ? [this.thread.upsert({ ...row, status: 'failed' as const, finishedAt: now })] : []),
|
|
1518
1742
|
this.thread.upsert({
|
|
@@ -1524,6 +1748,7 @@ export class ChatSession {
|
|
|
1524
1748
|
text: `"${row.description || row.subagentType || 'Sub-agent'}" was stopped.`
|
|
1525
1749
|
})
|
|
1526
1750
|
]);
|
|
1751
|
+
|
|
1527
1752
|
this.options.persist();
|
|
1528
1753
|
}
|
|
1529
1754
|
|
|
@@ -1535,10 +1760,13 @@ export class ChatSession {
|
|
|
1535
1760
|
if (this.thread.info.activeTurnId !== null) {
|
|
1536
1761
|
throw new ChatError('chat-busy', 'A turn is running; stop the turn instead');
|
|
1537
1762
|
}
|
|
1763
|
+
|
|
1538
1764
|
const row = this.thread.find('subagent', (item) => item.toolUseId === toolUseId);
|
|
1765
|
+
|
|
1539
1766
|
if (row?.status !== 'running') {
|
|
1540
1767
|
return;
|
|
1541
1768
|
}
|
|
1769
|
+
|
|
1542
1770
|
this.markStopped([row]);
|
|
1543
1771
|
}
|
|
1544
1772
|
|
|
@@ -1555,11 +1783,15 @@ export class ChatSession {
|
|
|
1555
1783
|
if (rows.length === 0) {
|
|
1556
1784
|
return;
|
|
1557
1785
|
}
|
|
1786
|
+
|
|
1558
1787
|
const now = Date.now();
|
|
1788
|
+
|
|
1559
1789
|
for (const row of rows) {
|
|
1560
1790
|
this.orphans.delete(row.toolUseId);
|
|
1561
1791
|
}
|
|
1792
|
+
|
|
1562
1793
|
const names = rows.map((row) => `"${row.description || row.subagentType || 'Sub-agent'}"`).join(', ');
|
|
1794
|
+
|
|
1563
1795
|
this.emit([
|
|
1564
1796
|
...rows.map((row) => this.thread.upsert({ ...row, status: 'failed', finishedAt: now })),
|
|
1565
1797
|
this.thread.upsert({
|
|
@@ -1571,12 +1803,14 @@ export class ChatSession {
|
|
|
1571
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.`
|
|
1572
1804
|
})
|
|
1573
1805
|
]);
|
|
1806
|
+
|
|
1574
1807
|
this.options.persist();
|
|
1575
1808
|
}
|
|
1576
1809
|
|
|
1577
1810
|
/* Asks the transcript again for a row a load left running, when somebody looks at it. */
|
|
1578
1811
|
async recheckOrphan(toolUseId: string): Promise<void> {
|
|
1579
1812
|
const row = this.thread.find('subagent', (item) => item.toolUseId === toolUseId);
|
|
1813
|
+
|
|
1580
1814
|
if (this.orphans.has(toolUseId) && (row?.status === 'running' || row?.status === 'failed')) {
|
|
1581
1815
|
await this.settleFromTranscripts([row]);
|
|
1582
1816
|
}
|
|
@@ -1585,9 +1819,11 @@ export class ChatSession {
|
|
|
1585
1819
|
/* Writes down where a subagent's conversation was found, so the next question about it reads no folder. */
|
|
1586
1820
|
noteSubagentNative(toolUseId: string, native: { agentId?: string; threadId?: string }): void {
|
|
1587
1821
|
const item = this.thread.find('subagent', (candidate) => candidate.toolUseId === toolUseId);
|
|
1822
|
+
|
|
1588
1823
|
if (!item || (item.native?.agentId === native.agentId && item.native?.threadId === native.threadId)) {
|
|
1589
1824
|
return;
|
|
1590
1825
|
}
|
|
1826
|
+
|
|
1591
1827
|
this.emit([this.thread.upsert({ ...item, native: { ...item.native, ...native } })]);
|
|
1592
1828
|
this.options.persistSoon();
|
|
1593
1829
|
}
|
|
@@ -1599,9 +1835,11 @@ export class ChatSession {
|
|
|
1599
1835
|
*/
|
|
1600
1836
|
async skills(scan: () => Promise<ChatSkill[]>): Promise<ChatSkill[]> {
|
|
1601
1837
|
const backend = this.backend;
|
|
1838
|
+
|
|
1602
1839
|
if (backend?.running === true && backend.listSkills) {
|
|
1603
1840
|
try {
|
|
1604
1841
|
const own = await backend.listSkills();
|
|
1842
|
+
|
|
1605
1843
|
if (own.length > 0) {
|
|
1606
1844
|
return own;
|
|
1607
1845
|
}
|
|
@@ -1609,12 +1847,16 @@ export class ChatSession {
|
|
|
1609
1847
|
// The CLI could not answer; the scan below is what is left.
|
|
1610
1848
|
}
|
|
1611
1849
|
}
|
|
1850
|
+
|
|
1612
1851
|
const scanned = await scan();
|
|
1613
1852
|
const announced = this.thread.info.skills;
|
|
1853
|
+
|
|
1614
1854
|
if (!announced?.length) {
|
|
1615
1855
|
return scanned;
|
|
1616
1856
|
}
|
|
1857
|
+
|
|
1617
1858
|
const known = new Set(announced);
|
|
1859
|
+
|
|
1618
1860
|
return scanned.filter((skill) => known.has(skill.name));
|
|
1619
1861
|
}
|
|
1620
1862
|
|
|
@@ -1623,14 +1865,17 @@ export class ChatSession {
|
|
|
1623
1865
|
const turnId = requestedTurnId ?? newId('turn');
|
|
1624
1866
|
const now = Date.now();
|
|
1625
1867
|
const events = [this.thread.upsert({ id: turnId, kind: 'turn', createdAt: now, turnId, state: 'running', origin: 'user', endedAt: null, costUsd: 0 })];
|
|
1868
|
+
|
|
1626
1869
|
if (note !== null) {
|
|
1627
1870
|
events.push(this.thread.upsert({ id: newId('note'), kind: 'note', createdAt: now, turnId, level: 'info', text: note }));
|
|
1628
1871
|
}
|
|
1872
|
+
|
|
1629
1873
|
if (text !== null) {
|
|
1630
1874
|
const mentions = extras.mentions?.length ? extras.mentions : undefined;
|
|
1631
1875
|
const skills = extras.skills?.length ? extras.skills : undefined;
|
|
1632
1876
|
const chats = extras.chats?.length ? extras.chats : undefined;
|
|
1633
1877
|
const attachments = extras.attachments?.length ? extras.attachments : undefined;
|
|
1878
|
+
|
|
1634
1879
|
events.push(
|
|
1635
1880
|
this.thread.upsert({
|
|
1636
1881
|
id: newId('user'),
|
|
@@ -1648,13 +1893,17 @@ export class ChatSession {
|
|
|
1648
1893
|
})
|
|
1649
1894
|
);
|
|
1650
1895
|
}
|
|
1896
|
+
|
|
1651
1897
|
events.push(this.thread.patchInfo({ status: this.thread.statusFor(turnId), activeTurnId: turnId }));
|
|
1652
1898
|
this.emit(events);
|
|
1899
|
+
|
|
1653
1900
|
if (extras.uiChoice) {
|
|
1654
1901
|
this.recordUiAnswer({ ...extras.uiChoice, turnId, queued: false });
|
|
1655
1902
|
}
|
|
1903
|
+
|
|
1656
1904
|
// On disk before the CLI answers, so a host that goes down mid-turn still shows the question.
|
|
1657
1905
|
this.options.persist();
|
|
1906
|
+
|
|
1658
1907
|
return turnId;
|
|
1659
1908
|
}
|
|
1660
1909
|
|
|
@@ -1665,12 +1914,15 @@ export class ChatSession {
|
|
|
1665
1914
|
private settleAgentTurn(): void {
|
|
1666
1915
|
const turnId = this.thread.info.activeTurnId;
|
|
1667
1916
|
const turn = turnId ? this.thread.get(turnId) : undefined;
|
|
1917
|
+
|
|
1668
1918
|
if (turn?.kind !== 'turn' || turn.origin !== 'agent') {
|
|
1669
1919
|
return;
|
|
1670
1920
|
}
|
|
1921
|
+
|
|
1671
1922
|
if (this.spokenTurnId === turn.id) {
|
|
1672
1923
|
this.staleResults += 1;
|
|
1673
1924
|
}
|
|
1925
|
+
|
|
1674
1926
|
this.emit([this.thread.upsert({ ...turn, state: 'done', endedAt: Date.now() }), this.thread.patchInfo({ status: 'idle', activeTurnId: null })]);
|
|
1675
1927
|
this.settleCheckpoint(turn.id);
|
|
1676
1928
|
}
|
|
@@ -1678,16 +1930,21 @@ export class ChatSession {
|
|
|
1678
1930
|
/* Records the folder's tree on the turn; a folder without git leaves the turn as it is. */
|
|
1679
1931
|
private checkpoint(turnId: string): Promise<void> {
|
|
1680
1932
|
const checkpoints = this.options.checkpoints;
|
|
1933
|
+
|
|
1681
1934
|
if (!checkpoints) {
|
|
1682
1935
|
return Promise.resolve();
|
|
1683
1936
|
}
|
|
1937
|
+
|
|
1684
1938
|
const cwd = this.thread.info.cwd;
|
|
1939
|
+
|
|
1685
1940
|
return this.trackCheckpoint(
|
|
1686
1941
|
Promise.all([checkpoints.take(cwd), checkpoints.head?.(cwd) ?? null]).then(([tree, head]) => {
|
|
1687
1942
|
const turn = this.thread.get(turnId);
|
|
1943
|
+
|
|
1688
1944
|
if (tree === null || turn?.kind !== 'turn' || turn.state !== 'running' || this.info.activeTurnId !== turnId) {
|
|
1689
1945
|
return;
|
|
1690
1946
|
}
|
|
1947
|
+
|
|
1691
1948
|
this.emit([this.thread.upsert({ ...turn, checkpoint: tree, ...(head === null ? {} : { checkpointHead: head }) })]);
|
|
1692
1949
|
})
|
|
1693
1950
|
);
|
|
@@ -1696,17 +1953,22 @@ export class ChatSession {
|
|
|
1696
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. */
|
|
1697
1954
|
private settleCheckpoint(turnId: string): void {
|
|
1698
1955
|
const turn = this.thread.get(turnId);
|
|
1956
|
+
|
|
1699
1957
|
if (turn?.kind !== 'turn' || turn.checkpoint === undefined || turn.checkpointDiff || !this.options.checkpoints) {
|
|
1700
1958
|
return;
|
|
1701
1959
|
}
|
|
1960
|
+
|
|
1702
1961
|
const { checkpoints } = this.options;
|
|
1703
1962
|
const cwd = this.thread.info.cwd;
|
|
1963
|
+
|
|
1704
1964
|
void this.trackCheckpoint(
|
|
1705
1965
|
Promise.all([checkpoints.settle(cwd, turn.checkpoint), checkpoints.head?.(cwd) ?? null]).then(([answer, head]) => {
|
|
1706
1966
|
const settled = this.thread.get(turnId);
|
|
1967
|
+
|
|
1707
1968
|
if (answer === null || settled?.kind !== 'turn') {
|
|
1708
1969
|
return;
|
|
1709
1970
|
}
|
|
1971
|
+
|
|
1710
1972
|
this.emit([
|
|
1711
1973
|
this.thread.upsert({
|
|
1712
1974
|
...settled,
|
|
@@ -1715,6 +1977,7 @@ export class ChatSession {
|
|
|
1715
1977
|
...(head === null ? {} : { checkpointHeadAfter: head })
|
|
1716
1978
|
})
|
|
1717
1979
|
]);
|
|
1980
|
+
|
|
1718
1981
|
this.options.persist();
|
|
1719
1982
|
})
|
|
1720
1983
|
);
|
|
@@ -1724,6 +1987,7 @@ export class ChatSession {
|
|
|
1724
1987
|
private trackCheckpoint(work: Promise<void>): Promise<void> {
|
|
1725
1988
|
const tracked = work.catch(() => undefined).finally(() => this.checkpointWork.delete(tracked));
|
|
1726
1989
|
this.checkpointWork.add(tracked);
|
|
1990
|
+
|
|
1727
1991
|
return tracked;
|
|
1728
1992
|
}
|
|
1729
1993
|
|
|
@@ -1736,6 +2000,7 @@ export class ChatSession {
|
|
|
1736
2000
|
if (text.startsWith('/')) {
|
|
1737
2001
|
return { preamble: null, note: null };
|
|
1738
2002
|
}
|
|
2003
|
+
|
|
1739
2004
|
const host = this.options.promptNotes?.next() ?? { shown: [], heard: [] };
|
|
1740
2005
|
// Keep consumed notes in the submission until the CLI acknowledges them.
|
|
1741
2006
|
const preambles = this.preambles;
|
|
@@ -1744,7 +2009,9 @@ export class ChatSession {
|
|
|
1744
2009
|
const hints = this.pendingHints;
|
|
1745
2010
|
this.pendingHints = [];
|
|
1746
2011
|
const read = [...preambles, ...host.shown];
|
|
2012
|
+
|
|
1747
2013
|
const joined = (parts: string[]): string | null => (parts.length === 0 ? null : parts.join('\n\n'));
|
|
2014
|
+
|
|
1748
2015
|
return { preamble: joined([...read, ...host.heard, ...hints]), note: joined(read) };
|
|
1749
2016
|
}
|
|
1750
2017
|
|
|
@@ -1753,8 +2020,10 @@ export class ChatSession {
|
|
|
1753
2020
|
this.submission = { turnId, input: { ...input, promptId: randomUUID() } };
|
|
1754
2021
|
const submission = this.submission;
|
|
1755
2022
|
this.options.persist();
|
|
2023
|
+
|
|
1756
2024
|
this.run((backend) => {
|
|
1757
2025
|
backend.sendTurn(submission.input);
|
|
2026
|
+
|
|
1758
2027
|
if (!backend.acknowledgesTurns) {
|
|
1759
2028
|
this.acceptPrompt(submission.input.promptId!);
|
|
1760
2029
|
}
|
|
@@ -1763,43 +2032,54 @@ export class ChatSession {
|
|
|
1763
2032
|
|
|
1764
2033
|
private acceptPrompt(promptId: string): void {
|
|
1765
2034
|
const submission = this.submission;
|
|
2035
|
+
|
|
1766
2036
|
if (submission?.input.promptId !== promptId) {
|
|
1767
2037
|
return;
|
|
1768
2038
|
}
|
|
2039
|
+
|
|
1769
2040
|
this.submission = null;
|
|
1770
2041
|
const turn = this.thread.get(submission.turnId);
|
|
2042
|
+
|
|
1771
2043
|
if (turn?.kind === 'turn' && (turn.deliveryPending || turn.resumePending)) {
|
|
1772
2044
|
this.emit([this.thread.upsert({ ...turn, deliveryPending: undefined, resumePending: undefined })]);
|
|
1773
2045
|
}
|
|
2046
|
+
|
|
1774
2047
|
this.options.persist();
|
|
1775
2048
|
}
|
|
1776
2049
|
|
|
1777
2050
|
/* Runs one turn against the backend; a backend that will not start ends the turn with the reason. */
|
|
1778
2051
|
private run(work: (backend: ChatBackend) => void): void {
|
|
1779
2052
|
const turnId = this.info.activeTurnId;
|
|
2053
|
+
|
|
1780
2054
|
if (turnId === null) {
|
|
1781
2055
|
return;
|
|
1782
2056
|
}
|
|
2057
|
+
|
|
1783
2058
|
const pending = this.watchStart(turnId);
|
|
1784
2059
|
let backend: Promise<ChatBackend>;
|
|
2060
|
+
|
|
1785
2061
|
try {
|
|
1786
2062
|
backend = this.ensureBackend();
|
|
1787
2063
|
} catch (error) {
|
|
1788
2064
|
// What the host prepares for a launch (its folders, its instructions) can throw too; the turn already opened, so it fails.
|
|
1789
|
-
backend = Promise.reject(
|
|
2065
|
+
backend = Promise.reject(asError(error));
|
|
1790
2066
|
}
|
|
2067
|
+
|
|
1791
2068
|
const generation = this.generation;
|
|
2069
|
+
|
|
1792
2070
|
void Promise.all([backend, this.turnReady, this.options.save?.()])
|
|
1793
2071
|
.then(([started]) => {
|
|
1794
2072
|
if (this.pendingStart !== pending || this.backend !== started || this.info.activeTurnId !== turnId || this.frozen) {
|
|
1795
2073
|
return;
|
|
1796
2074
|
}
|
|
2075
|
+
|
|
1797
2076
|
this.clearStartWarning();
|
|
1798
2077
|
work(started);
|
|
1799
2078
|
})
|
|
1800
2079
|
.catch((error: unknown) => {
|
|
1801
2080
|
if (this.pendingStart === pending || (this.backend !== null && this.generation === generation && this.info.activeTurnId === turnId)) {
|
|
1802
2081
|
this.clearStartWarning();
|
|
2082
|
+
|
|
1803
2083
|
if (error instanceof ChatError && error.code === 'chat-busy') {
|
|
1804
2084
|
this.pendingPreambles = this.preambles;
|
|
1805
2085
|
this.submission = null;
|
|
@@ -1807,8 +2087,10 @@ export class ChatSession {
|
|
|
1807
2087
|
this.emit(this.projector.project(generation, { type: 'turn.done', state: 'error', costUsd: 0, error: error.message }));
|
|
1808
2088
|
this.settleCheckpoint(turnId);
|
|
1809
2089
|
this.options.persist();
|
|
2090
|
+
|
|
1810
2091
|
return;
|
|
1811
2092
|
}
|
|
2093
|
+
|
|
1812
2094
|
this.receive(generation, { type: 'failed', message: errorText(error), processAlive: this.running });
|
|
1813
2095
|
}
|
|
1814
2096
|
});
|
|
@@ -1817,6 +2099,7 @@ export class ChatSession {
|
|
|
1817
2099
|
private watchStart(turnId: string): { turnId: string; warning: unknown } {
|
|
1818
2100
|
this.clearStartWarning();
|
|
1819
2101
|
const clock = this.options.clock ?? systemClock;
|
|
2102
|
+
|
|
1820
2103
|
const pending = {
|
|
1821
2104
|
turnId,
|
|
1822
2105
|
warning: clock.setTimeout(() => {
|
|
@@ -1831,11 +2114,14 @@ export class ChatSession {
|
|
|
1831
2114
|
text: 'The agent is still starting. You can stop this turn and try again.'
|
|
1832
2115
|
})
|
|
1833
2116
|
]);
|
|
2117
|
+
|
|
1834
2118
|
this.options.persist();
|
|
1835
2119
|
}
|
|
1836
2120
|
}, 30_000)
|
|
1837
2121
|
};
|
|
2122
|
+
|
|
1838
2123
|
this.pendingStart = pending;
|
|
2124
|
+
|
|
1839
2125
|
return pending;
|
|
1840
2126
|
}
|
|
1841
2127
|
|
|
@@ -1843,6 +2129,7 @@ export class ChatSession {
|
|
|
1843
2129
|
if (this.pendingStart === null) {
|
|
1844
2130
|
return;
|
|
1845
2131
|
}
|
|
2132
|
+
|
|
1846
2133
|
(this.options.clock ?? systemClock).clearTimeout(this.pendingStart.warning);
|
|
1847
2134
|
this.pendingStart = null;
|
|
1848
2135
|
}
|
|
@@ -1881,27 +2168,34 @@ export class ChatSession {
|
|
|
1881
2168
|
if (this.processInUse()) {
|
|
1882
2169
|
return Promise.reject(new ChatError('chat-busy', 'The current work and open requests must finish before the changes to this chat can apply'));
|
|
1883
2170
|
}
|
|
2171
|
+
|
|
1884
2172
|
// Let the old one go quietly; its events are dropped and its exit must not end the chat.
|
|
1885
2173
|
this.backend.stop();
|
|
1886
2174
|
this.backend = null;
|
|
1887
2175
|
this.starting = null;
|
|
1888
2176
|
}
|
|
2177
|
+
|
|
1889
2178
|
if (this.backend && this.starting) {
|
|
1890
2179
|
return this.starting;
|
|
1891
2180
|
}
|
|
2181
|
+
|
|
1892
2182
|
const projectConfig = this.loadsProjectConfig();
|
|
2183
|
+
|
|
1893
2184
|
if (!projectConfig && !this.options.provider.ignoresProjectConfig) {
|
|
1894
2185
|
return Promise.reject(
|
|
1895
2186
|
new ChatError('project-config-unsupported', `${this.options.provider.name} cannot run without the configuration of its folder`)
|
|
1896
2187
|
);
|
|
1897
2188
|
}
|
|
2189
|
+
|
|
1898
2190
|
let env: Record<string, string>;
|
|
2191
|
+
|
|
1899
2192
|
try {
|
|
1900
2193
|
env = this.options.env(this.thread.info.account);
|
|
1901
2194
|
this.launchedAccount = this.thread.info.account;
|
|
1902
2195
|
} catch (error) {
|
|
1903
|
-
return Promise.reject(
|
|
2196
|
+
return Promise.reject(asError(error));
|
|
1904
2197
|
}
|
|
2198
|
+
|
|
1905
2199
|
this.generation = nextGeneration();
|
|
1906
2200
|
const generation = this.generation;
|
|
1907
2201
|
const info = this.thread.info;
|
|
@@ -1911,6 +2205,7 @@ export class ChatSession {
|
|
|
1911
2205
|
const folders = this.options.folders?.() ?? [];
|
|
1912
2206
|
this.launchedFolders = folders;
|
|
1913
2207
|
this.launchedProjectConfig = projectConfig;
|
|
2208
|
+
|
|
1914
2209
|
const launch: BackendLaunch = {
|
|
1915
2210
|
command: this.options.command,
|
|
1916
2211
|
cwd: info.cwd,
|
|
@@ -1931,7 +2226,9 @@ export class ChatSession {
|
|
|
1931
2226
|
...(projectConfig ? {} : { projectConfig }),
|
|
1932
2227
|
...(this.options.spawn ? { spawn: this.options.spawn } : {})
|
|
1933
2228
|
};
|
|
2229
|
+
|
|
1934
2230
|
const made: { backend: ChatBackend | null } = { backend: null };
|
|
2231
|
+
|
|
1935
2232
|
try {
|
|
1936
2233
|
made.backend = this.options.provider.createBackend(launch, {
|
|
1937
2234
|
onEvent: (event) => {
|
|
@@ -1941,19 +2238,22 @@ export class ChatSession {
|
|
|
1941
2238
|
}
|
|
1942
2239
|
});
|
|
1943
2240
|
} catch (error) {
|
|
1944
|
-
return Promise.reject(
|
|
2241
|
+
return Promise.reject(asError(error));
|
|
1945
2242
|
}
|
|
2243
|
+
|
|
1946
2244
|
const backend = made.backend;
|
|
1947
2245
|
this.backend = backend;
|
|
1948
2246
|
this.replaceProcess = false;
|
|
1949
2247
|
this.emit([this.thread.patchInfo({ running: true, effectiveRuntimeMode: undefined, permissionMode: undefined })]);
|
|
1950
2248
|
let started: Promise<void>;
|
|
2249
|
+
|
|
1951
2250
|
try {
|
|
1952
2251
|
started = backend.start();
|
|
1953
2252
|
} catch (error) {
|
|
1954
2253
|
// A spawn that throws on the spot (no such executable) is a start that failed like any other.
|
|
1955
|
-
started = Promise.reject(
|
|
2254
|
+
started = Promise.reject(asError(error));
|
|
1956
2255
|
}
|
|
2256
|
+
|
|
1957
2257
|
this.starting = started
|
|
1958
2258
|
.then(() => backend)
|
|
1959
2259
|
.catch((error: unknown) => {
|
|
@@ -1962,13 +2262,16 @@ export class ChatSession {
|
|
|
1962
2262
|
this.backend = null;
|
|
1963
2263
|
this.starting = null;
|
|
1964
2264
|
}
|
|
2265
|
+
|
|
1965
2266
|
throw error;
|
|
1966
2267
|
});
|
|
2268
|
+
|
|
1967
2269
|
return this.starting;
|
|
1968
2270
|
}
|
|
1969
2271
|
|
|
1970
2272
|
private awaitsResume(turnId: string, attempt: number): boolean {
|
|
1971
2273
|
const turn = this.thread.get(turnId);
|
|
2274
|
+
|
|
1972
2275
|
return (
|
|
1973
2276
|
!this.frozen &&
|
|
1974
2277
|
turn?.kind === 'turn' &&
|
|
@@ -1982,33 +2285,45 @@ export class ChatSession {
|
|
|
1982
2285
|
if (this.frozen) {
|
|
1983
2286
|
return;
|
|
1984
2287
|
}
|
|
2288
|
+
|
|
1985
2289
|
if (event.type === 'turn.accepted') {
|
|
1986
2290
|
this.acceptPrompt(event.promptId);
|
|
2291
|
+
|
|
1987
2292
|
return;
|
|
1988
2293
|
}
|
|
2294
|
+
|
|
1989
2295
|
if (event.type === 'limits') {
|
|
1990
2296
|
this.options.onLimits?.(this.launchedAccount === undefined ? event.update : { ...event.update, account: this.launchedAccount });
|
|
2297
|
+
|
|
1991
2298
|
return;
|
|
1992
2299
|
}
|
|
2300
|
+
|
|
1993
2301
|
if (event.type === 'title') {
|
|
1994
2302
|
this.applyTitle(event.title);
|
|
2303
|
+
|
|
1995
2304
|
return;
|
|
1996
2305
|
}
|
|
2306
|
+
|
|
1997
2307
|
if (event.type === 'stop.refused') {
|
|
1998
2308
|
this.refuseStop(event.taskId, event.message);
|
|
2309
|
+
|
|
1999
2310
|
return;
|
|
2000
2311
|
}
|
|
2312
|
+
|
|
2001
2313
|
/*
|
|
2002
2314
|
* A CLI that counts its tokens but names no window, in a chat that has none yet: the catalog may have learned the
|
|
2003
2315
|
* model's window since the chat began, as an ACP agent's catalog does from the first session that reports its models.
|
|
2004
2316
|
*/
|
|
2005
2317
|
if (event.type === 'usage' && event.contextWindow === undefined && event.contextTokens !== undefined && this.thread.info.usage.contextWindow === null) {
|
|
2006
2318
|
const contextWindow = this.options.provider.catalog.contextWindowFor(this.launchedSelection ?? this.thread.info.selection);
|
|
2319
|
+
|
|
2007
2320
|
if (contextWindow !== null) {
|
|
2008
2321
|
this.emit(this.projector.project(generation, { ...event, contextWindow }));
|
|
2322
|
+
|
|
2009
2323
|
return;
|
|
2010
2324
|
}
|
|
2011
2325
|
}
|
|
2326
|
+
|
|
2012
2327
|
/*
|
|
2013
2328
|
* A CLI reports the window it was started on. A pick made since waits for the restart the next
|
|
2014
2329
|
* send does, so its report may not put the old size back under a meter that already reads the new one.
|
|
@@ -2017,66 +2332,88 @@ export class ChatSession {
|
|
|
2017
2332
|
if (event.contextTokens === undefined) {
|
|
2018
2333
|
return;
|
|
2019
2334
|
}
|
|
2335
|
+
|
|
2020
2336
|
this.emit(this.projector.project(generation, { type: 'usage', contextTokens: event.contextTokens }));
|
|
2337
|
+
|
|
2021
2338
|
return;
|
|
2022
2339
|
}
|
|
2340
|
+
|
|
2023
2341
|
if (event.type === 'exit') {
|
|
2024
2342
|
this.backend = null;
|
|
2025
2343
|
this.starting = null;
|
|
2026
2344
|
this.awaitingBackgroundReport = false;
|
|
2027
2345
|
}
|
|
2346
|
+
|
|
2028
2347
|
// A turn we settled ourselves still has its own `result` coming; it may not close the turn after it.
|
|
2029
2348
|
if (event.type === 'turn.done' && event.promptId === undefined && this.staleResults > 0) {
|
|
2030
2349
|
this.staleResults -= 1;
|
|
2350
|
+
|
|
2031
2351
|
return;
|
|
2032
2352
|
}
|
|
2353
|
+
|
|
2033
2354
|
if (event.type === 'exit' || event.type === 'failed') {
|
|
2034
2355
|
this.staleResults = 0;
|
|
2035
2356
|
}
|
|
2357
|
+
|
|
2036
2358
|
if (event.type === 'turn.done') {
|
|
2037
2359
|
if (event.promptId !== undefined && this.submission !== null && this.submission.input.promptId !== event.promptId) {
|
|
2038
2360
|
return;
|
|
2039
2361
|
}
|
|
2362
|
+
|
|
2040
2363
|
if (event.promptId !== undefined) {
|
|
2041
2364
|
this.staleResults = 0;
|
|
2042
2365
|
}
|
|
2366
|
+
|
|
2043
2367
|
if (event.replaceProcess === true) {
|
|
2044
2368
|
this.replaceProcess = true;
|
|
2045
2369
|
}
|
|
2370
|
+
|
|
2046
2371
|
const promptId = event.promptId ?? this.submission?.input.promptId;
|
|
2372
|
+
|
|
2047
2373
|
if (promptId !== undefined) {
|
|
2048
2374
|
this.acceptPrompt(promptId);
|
|
2049
2375
|
}
|
|
2050
2376
|
}
|
|
2377
|
+
|
|
2051
2378
|
const openTurnId = this.thread.info.activeTurnId;
|
|
2379
|
+
|
|
2052
2380
|
if ((event.type === 'turn.done' && event.state !== 'done') || ((event.type === 'failed' || event.type === 'exit') && openTurnId !== null)) {
|
|
2053
2381
|
this.pauseQueue(true);
|
|
2054
2382
|
}
|
|
2383
|
+
|
|
2055
2384
|
// Settled as failed by the exit below; the transcript may still show that they finished first.
|
|
2056
2385
|
const orphaned = event.type === 'exit' && this.info.provider === 'claude' ? this.runningBackgroundRows() : [];
|
|
2386
|
+
|
|
2057
2387
|
if (event.type === 'turn.done' && event.state === 'aborted') {
|
|
2058
2388
|
// A request of work beside the turn outlives the interrupt; turned down, that work goes on instead of waiting forever.
|
|
2059
2389
|
for (const item of this.thread.pending()) {
|
|
2060
2390
|
this.backend?.declineRequest?.(item.requestId, 'The user stopped the turn');
|
|
2061
2391
|
}
|
|
2062
2392
|
}
|
|
2393
|
+
|
|
2063
2394
|
const hadNativeWork = (event.type === 'task.done' || event.type === 'tool.done' || event.type === 'background.ended') && this.hasNativeWork();
|
|
2064
2395
|
const projected = this.projector.project(generation, event);
|
|
2396
|
+
|
|
2065
2397
|
if (event.type === 'turn.done' || (event.type === 'failed' && event.processAlive !== true)) {
|
|
2066
2398
|
this.awaitingBackgroundReport = false;
|
|
2067
2399
|
} else if (event.type !== 'exit' && event.type !== 'failed' && hadNativeWork && !this.hasNativeWork() && reportsOnBackgroundWork(this.info.provider)) {
|
|
2068
2400
|
this.awaitingBackgroundReport = true;
|
|
2069
2401
|
}
|
|
2402
|
+
|
|
2070
2403
|
this.emit(projected);
|
|
2071
2404
|
this.trackSubagentOwner(generation, event);
|
|
2072
2405
|
this.watchWorkflows(generation, event);
|
|
2406
|
+
|
|
2073
2407
|
if (isMainAgentOutput(event)) {
|
|
2074
2408
|
this.spokenTurnId = this.thread.info.activeTurnId;
|
|
2075
2409
|
}
|
|
2410
|
+
|
|
2076
2411
|
if (orphaned.length > 0) {
|
|
2077
2412
|
void this.settleFromTranscripts(orphaned.flatMap((row) => this.thread.get(row.id) ?? []).filter((item) => item.kind === 'subagent'));
|
|
2078
2413
|
}
|
|
2414
|
+
|
|
2079
2415
|
const activeTurnId = this.thread.info.activeTurnId;
|
|
2416
|
+
|
|
2080
2417
|
if (openTurnId === null && activeTurnId !== null) {
|
|
2081
2418
|
// The CLI opened this turn itself; it takes a checkpoint like any other, so the card can
|
|
2082
2419
|
// show what the agent changed while nobody was watching.
|
|
@@ -2084,20 +2421,26 @@ export class ChatSession {
|
|
|
2084
2421
|
void this.checkpoint(activeTurnId);
|
|
2085
2422
|
this.options.persist();
|
|
2086
2423
|
}
|
|
2424
|
+
|
|
2087
2425
|
if (event.type === 'turn.done' || event.type === 'exit' || event.type === 'failed') {
|
|
2088
2426
|
this.options.persist();
|
|
2089
2427
|
}
|
|
2428
|
+
|
|
2090
2429
|
if (event.type === 'permissions' || event.type === 'session') {
|
|
2091
2430
|
this.options.persist();
|
|
2092
2431
|
}
|
|
2432
|
+
|
|
2093
2433
|
if (openTurnId !== null && activeTurnId === null) {
|
|
2094
2434
|
this.settleCheckpoint(openTurnId);
|
|
2095
2435
|
}
|
|
2436
|
+
|
|
2096
2437
|
if (event.type === 'session' && typeof event.title === 'string') {
|
|
2097
2438
|
this.applyTitle(event.title);
|
|
2098
2439
|
}
|
|
2440
|
+
|
|
2099
2441
|
if (event.type === 'session' && this.thread.info.agentSessionId !== null) {
|
|
2100
2442
|
this.refreshTitle();
|
|
2443
|
+
|
|
2101
2444
|
if (this.titleTimer === null && this.thread.info.suggestedTitle === undefined) {
|
|
2102
2445
|
this.titleTimer = setTimeout(() => {
|
|
2103
2446
|
this.titleTimer = null;
|
|
@@ -2105,15 +2448,19 @@ export class ChatSession {
|
|
|
2105
2448
|
}, TITLE_RECHECK_MS);
|
|
2106
2449
|
}
|
|
2107
2450
|
}
|
|
2451
|
+
|
|
2108
2452
|
// The turn is over and the CLI is still there, so whatever waited behind it can go out now.
|
|
2109
2453
|
if (event.type === 'turn.done') {
|
|
2110
2454
|
this.refreshTitle();
|
|
2455
|
+
|
|
2111
2456
|
if (event.state === 'done') {
|
|
2112
2457
|
this.nameThread();
|
|
2113
2458
|
}
|
|
2459
|
+
|
|
2114
2460
|
if (event.limit !== undefined) {
|
|
2115
2461
|
this.oweResume(true);
|
|
2116
2462
|
}
|
|
2463
|
+
|
|
2117
2464
|
this.drainQueue(event.state === 'aborted' && this.sendAfterStop);
|
|
2118
2465
|
this.sendAfterStop = false;
|
|
2119
2466
|
} else if (
|
|
@@ -2133,9 +2480,11 @@ export class ChatSession {
|
|
|
2133
2480
|
|
|
2134
2481
|
private clearWorkflowWarnings(): void {
|
|
2135
2482
|
const clock = this.options.clock ?? systemClock;
|
|
2483
|
+
|
|
2136
2484
|
for (const timer of this.workflowWarnings.values()) {
|
|
2137
2485
|
clock.clearTimeout(timer);
|
|
2138
2486
|
}
|
|
2487
|
+
|
|
2139
2488
|
this.workflowWarnings.clear();
|
|
2140
2489
|
}
|
|
2141
2490
|
|
|
@@ -2143,15 +2492,20 @@ export class ChatSession {
|
|
|
2143
2492
|
if (event.type === 'tool.output' || event.type === 'tool.progress') {
|
|
2144
2493
|
// These come by the thousand, and only the call they name can be a workflow they move.
|
|
2145
2494
|
const item = this.projector.itemOf(generation, event.ref);
|
|
2495
|
+
|
|
2146
2496
|
if (item?.kind === 'tool' && item.name === 'Workflow') {
|
|
2147
2497
|
this.watchWorkflow(item, true);
|
|
2148
2498
|
}
|
|
2499
|
+
|
|
2149
2500
|
return;
|
|
2150
2501
|
}
|
|
2502
|
+
|
|
2151
2503
|
if (!['tool.started', 'tool.done', 'workflow.progress', 'task.started', 'task.done', 'exit', 'failed'].includes(event.type)) {
|
|
2152
2504
|
return;
|
|
2153
2505
|
}
|
|
2506
|
+
|
|
2154
2507
|
const ref = 'ref' in event ? event.ref : null;
|
|
2508
|
+
|
|
2155
2509
|
for (const item of this.thread.list()) {
|
|
2156
2510
|
if (item.kind === 'tool' && item.name === 'Workflow') {
|
|
2157
2511
|
this.watchWorkflow(item, ref === item.toolUseId && ['workflow.progress', 'tool.started', 'tool.done', 'task.started'].includes(event.type));
|
|
@@ -2162,15 +2516,18 @@ export class ChatSession {
|
|
|
2162
2516
|
private watchWorkflow(item: ChatToolItem, progress: boolean): void {
|
|
2163
2517
|
const clock = this.options.clock ?? systemClock;
|
|
2164
2518
|
const existing = this.workflowWarnings.get(item.id);
|
|
2519
|
+
|
|
2165
2520
|
if (item.state !== 'running' || progress) {
|
|
2166
2521
|
if (existing !== undefined) {
|
|
2167
2522
|
clock.clearTimeout(existing);
|
|
2168
2523
|
this.workflowWarnings.delete(item.id);
|
|
2169
2524
|
}
|
|
2170
2525
|
}
|
|
2526
|
+
|
|
2171
2527
|
if (item.state !== 'running' || (!progress && existing !== undefined) || (!progress && item.workflow?.stalledAt !== undefined)) {
|
|
2172
2528
|
return;
|
|
2173
2529
|
}
|
|
2530
|
+
|
|
2174
2531
|
if (progress) {
|
|
2175
2532
|
this.emit([
|
|
2176
2533
|
this.thread.upsert({
|
|
@@ -2179,14 +2536,17 @@ export class ChatSession {
|
|
|
2179
2536
|
})
|
|
2180
2537
|
]);
|
|
2181
2538
|
}
|
|
2539
|
+
|
|
2182
2540
|
this.workflowWarnings.set(
|
|
2183
2541
|
item.id,
|
|
2184
2542
|
clock.setTimeout(() => {
|
|
2185
2543
|
this.workflowWarnings.delete(item.id);
|
|
2186
2544
|
const current = this.thread.get(item.id);
|
|
2545
|
+
|
|
2187
2546
|
if (this.frozen || current?.kind !== 'tool' || current.state !== 'running') {
|
|
2188
2547
|
return;
|
|
2189
2548
|
}
|
|
2549
|
+
|
|
2190
2550
|
this.emit([this.thread.upsert({ ...current, workflow: { name: null, phases: [], agents: [], ...current.workflow, stalledAt: clock.now() } })]);
|
|
2191
2551
|
this.options.persist();
|
|
2192
2552
|
}, 5 * 60_000)
|
|
@@ -2196,15 +2556,18 @@ export class ChatSession {
|
|
|
2196
2556
|
/* Whether the CLI that reports was started on a pick standing for the same window as the chat's own. */
|
|
2197
2557
|
private reportsCurrentWindow(): boolean {
|
|
2198
2558
|
const catalog = this.options.provider.catalog;
|
|
2559
|
+
|
|
2199
2560
|
return this.launchedSelection === null || catalog.contextWindowFor(this.launchedSelection) === catalog.contextWindowFor(this.thread.info.selection);
|
|
2200
2561
|
}
|
|
2201
2562
|
|
|
2202
2563
|
/* Rides on the info patch: a client that has the chat open hears it, and a reload finds it stored. */
|
|
2203
2564
|
private refreshTitle(): void {
|
|
2204
2565
|
const agentSessionId = this.thread.info.agentSessionId;
|
|
2566
|
+
|
|
2205
2567
|
if (!this.options.readTitle || agentSessionId === null) {
|
|
2206
2568
|
return;
|
|
2207
2569
|
}
|
|
2570
|
+
|
|
2208
2571
|
void this.options
|
|
2209
2572
|
.readTitle(agentSessionId)
|
|
2210
2573
|
.then((title) => {
|
|
@@ -2212,6 +2575,7 @@ export class ChatSession {
|
|
|
2212
2575
|
if (title === null || title === this.thread.info.suggestedTitle || this.thread.info.agentSessionId !== agentSessionId) {
|
|
2213
2576
|
return;
|
|
2214
2577
|
}
|
|
2578
|
+
|
|
2215
2579
|
this.emit([this.thread.patchInfo({ suggestedTitle: title })]);
|
|
2216
2580
|
this.options.persistSoon();
|
|
2217
2581
|
})
|
|
@@ -2221,9 +2585,11 @@ export class ChatSession {
|
|
|
2221
2585
|
private applyTitle(raw: string): void {
|
|
2222
2586
|
// Every info a client reads carries the title, so one it would refuse would leave the chat stuck there.
|
|
2223
2587
|
const title = cleanTitle(raw);
|
|
2588
|
+
|
|
2224
2589
|
if (title === null || title === this.thread.info.suggestedTitle) {
|
|
2225
2590
|
return;
|
|
2226
2591
|
}
|
|
2592
|
+
|
|
2227
2593
|
this.emit([this.thread.patchInfo({ suggestedTitle: title })]);
|
|
2228
2594
|
this.options.persistSoon();
|
|
2229
2595
|
}
|
|
@@ -2235,28 +2601,36 @@ export class ChatSession {
|
|
|
2235
2601
|
*/
|
|
2236
2602
|
private nameThread(): void {
|
|
2237
2603
|
const name = this.options.nameThread;
|
|
2604
|
+
|
|
2238
2605
|
if (!name || this.naming || this.thread.info.suggestedTitle !== undefined) {
|
|
2239
2606
|
return;
|
|
2240
2607
|
}
|
|
2608
|
+
|
|
2241
2609
|
const items = this.thread.list();
|
|
2242
2610
|
const done = items.filter((item) => item.kind === 'turn' && item.state === 'done');
|
|
2243
2611
|
const prompt = items.find((item) => item.kind === 'user');
|
|
2612
|
+
|
|
2244
2613
|
if (done.length !== 1 || prompt?.kind !== 'user' || prompt.text.trim() === '') {
|
|
2245
2614
|
return;
|
|
2246
2615
|
}
|
|
2616
|
+
|
|
2247
2617
|
this.naming = true;
|
|
2248
2618
|
const turnId = done[0]!.id;
|
|
2619
|
+
|
|
2249
2620
|
const answer = items
|
|
2250
2621
|
.map((item) => (item.kind === 'assistant' && item.turnId === turnId ? readableAssistantText(item) : ''))
|
|
2251
2622
|
.filter((text) => text !== '')
|
|
2252
2623
|
.join('\n\n');
|
|
2624
|
+
|
|
2253
2625
|
const agentSessionId = this.thread.info.agentSessionId;
|
|
2626
|
+
|
|
2254
2627
|
void name({ cwd: this.thread.info.cwd, prompt: prompt.text, answer })
|
|
2255
2628
|
.then((title) => {
|
|
2256
2629
|
// A clear in between started another conversation, and a name that came meanwhile is the CLI's own.
|
|
2257
2630
|
if (title === null || this.thread.info.agentSessionId !== agentSessionId || this.thread.info.suggestedTitle !== undefined) {
|
|
2258
2631
|
return;
|
|
2259
2632
|
}
|
|
2633
|
+
|
|
2260
2634
|
this.applyTitle(title);
|
|
2261
2635
|
this.backend?.setTitle?.(title);
|
|
2262
2636
|
})
|
|
@@ -2270,17 +2644,23 @@ export class ChatSession {
|
|
|
2270
2644
|
/* Done for every row whose transcript shows the end, unless something else settled the row while it was read. */
|
|
2271
2645
|
private async settleFromTranscripts(rows: ChatSubagentItem[]): Promise<void> {
|
|
2272
2646
|
const lookup = this.options.subagentSettlement;
|
|
2647
|
+
|
|
2273
2648
|
if (!lookup || rows.length === 0) {
|
|
2274
2649
|
return;
|
|
2275
2650
|
}
|
|
2651
|
+
|
|
2276
2652
|
const settled: ChatSubagentItem[] = [];
|
|
2653
|
+
|
|
2277
2654
|
for (const row of rows) {
|
|
2278
2655
|
const settlement = await lookup(row.toolUseId);
|
|
2279
2656
|
const current = this.thread.get(row.id);
|
|
2657
|
+
|
|
2280
2658
|
if (settlement === null || current?.kind !== 'subagent' || current.status !== row.status || current.finishedAt !== row.finishedAt) {
|
|
2281
2659
|
continue;
|
|
2282
2660
|
}
|
|
2661
|
+
|
|
2283
2662
|
this.orphans.delete(row.toolUseId);
|
|
2663
|
+
|
|
2284
2664
|
settled.push({
|
|
2285
2665
|
...current,
|
|
2286
2666
|
status: 'done',
|
|
@@ -2288,6 +2668,7 @@ export class ChatSession {
|
|
|
2288
2668
|
result: settlement.report ?? current.result
|
|
2289
2669
|
});
|
|
2290
2670
|
}
|
|
2671
|
+
|
|
2291
2672
|
this.settleRows(settled);
|
|
2292
2673
|
}
|
|
2293
2674
|
|
|
@@ -2295,12 +2676,14 @@ export class ChatSession {
|
|
|
2295
2676
|
if (this.frozen || rows.length === 0) {
|
|
2296
2677
|
return;
|
|
2297
2678
|
}
|
|
2679
|
+
|
|
2298
2680
|
this.emit(rows.map((row) => this.thread.upsert(row)));
|
|
2299
2681
|
this.options.persist();
|
|
2300
2682
|
}
|
|
2301
2683
|
|
|
2302
2684
|
private emit(events: ChatEvent[]): void {
|
|
2303
2685
|
const delegating = this.delegatingChange(events);
|
|
2686
|
+
|
|
2304
2687
|
for (const event of delegating === null ? events : [...events, delegating]) {
|
|
2305
2688
|
this.uiReplies.pass(event);
|
|
2306
2689
|
}
|
|
@@ -2311,10 +2694,13 @@ export class ChatSession {
|
|
|
2311
2694
|
if (!events.some((event) => event.type === 'item' && isBackgroundWork(event.item))) {
|
|
2312
2695
|
return null;
|
|
2313
2696
|
}
|
|
2697
|
+
|
|
2314
2698
|
const delegating = runningInBackground(this.thread.list()).length > 0;
|
|
2699
|
+
|
|
2315
2700
|
if (delegating === (this.thread.info.delegating ?? false)) {
|
|
2316
2701
|
return null;
|
|
2317
2702
|
}
|
|
2703
|
+
|
|
2318
2704
|
return this.thread.patchInfo({ delegating: delegating || undefined });
|
|
2319
2705
|
}
|
|
2320
2706
|
}
|
|
@@ -2330,18 +2716,23 @@ function settleStoredItem(item: ChatItem, resumeTurnId: string | null): ChatItem
|
|
|
2330
2716
|
if (item.kind === 'assistant' && item.streaming) {
|
|
2331
2717
|
return { ...item, streaming: false };
|
|
2332
2718
|
}
|
|
2719
|
+
|
|
2333
2720
|
if (item.kind === 'approval' && item.decision === 'pending') {
|
|
2334
2721
|
return { ...item, decision: 'cancelled' };
|
|
2335
2722
|
}
|
|
2723
|
+
|
|
2336
2724
|
if (item.kind === 'question' && item.state === 'pending') {
|
|
2337
2725
|
return { ...item, state: 'cancelled' };
|
|
2338
2726
|
}
|
|
2727
|
+
|
|
2339
2728
|
if (item.kind === 'tool' && item.state === 'running') {
|
|
2340
2729
|
return { ...item, state: 'error' };
|
|
2341
2730
|
}
|
|
2731
|
+
|
|
2342
2732
|
// Aborted rather than error: the turn did not fail, the machine went down under it.
|
|
2343
2733
|
if (item.kind === 'turn' && item.state === 'running' && item.id !== resumeTurnId) {
|
|
2344
2734
|
return { ...item, state: 'aborted', endedAt: item.endedAt ?? item.createdAt };
|
|
2345
2735
|
}
|
|
2736
|
+
|
|
2346
2737
|
return item;
|
|
2347
2738
|
}
|