@agi-fans/dsh-tui 0.22.0 → 0.23.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/LICENSE +26 -0
- package/lib/CHANGELOG.md +199 -1
- package/lib/chrome/box.d.ts +1 -18
- package/lib/chrome/box.js +2 -45
- package/lib/chrome/box.js.map +1 -1
- package/lib/chrome/code-highlight.js +4 -4
- package/lib/chrome/code-highlight.js.map +1 -1
- package/lib/chrome/command-output.d.ts +0 -1
- package/lib/chrome/command-output.js +11 -5
- package/lib/chrome/command-output.js.map +1 -1
- package/lib/chrome/goal-bar.js +1 -1
- package/lib/chrome/goal-bar.js.map +1 -1
- package/lib/chrome/main-screen-renderer.js +4 -3
- package/lib/chrome/main-screen-renderer.js.map +1 -1
- package/lib/chrome/markdown.d.ts +3 -3
- package/lib/chrome/markdown.js +85 -36
- package/lib/chrome/markdown.js.map +1 -1
- package/lib/chrome/md-style.d.ts +5 -2
- package/lib/chrome/md-style.js +13 -4
- package/lib/chrome/md-style.js.map +1 -1
- package/lib/chrome/renderer.d.ts +37 -1
- package/lib/chrome/renderer.js.map +1 -1
- package/lib/chrome/status-line.d.ts +3 -3
- package/lib/chrome/status-line.js +5 -5
- package/lib/chrome/status-line.js.map +1 -1
- package/lib/chrome/theme.d.ts +8 -3
- package/lib/chrome/theme.js +51 -153
- package/lib/chrome/theme.js.map +1 -1
- package/lib/commands/auth-account.d.ts +9 -0
- package/lib/commands/auth-account.js +202 -0
- package/lib/commands/auth-account.js.map +1 -0
- package/lib/commands/auth.js +238 -414
- package/lib/commands/auth.js.map +1 -1
- package/lib/commands/diff.js +22 -16
- package/lib/commands/diff.js.map +1 -1
- package/lib/commands/export.d.ts +0 -1
- package/lib/commands/export.js +97 -32
- package/lib/commands/export.js.map +1 -1
- package/lib/commands/files.js +2 -8
- package/lib/commands/files.js.map +1 -1
- package/lib/commands/integrations.d.ts +12 -1
- package/lib/commands/integrations.js +120 -23
- package/lib/commands/integrations.js.map +1 -1
- package/lib/commands/jobs-browser.d.ts +3 -0
- package/lib/commands/jobs-browser.js +152 -0
- package/lib/commands/jobs-browser.js.map +1 -0
- package/lib/commands/jobs.d.ts +3 -14
- package/lib/commands/jobs.js +26 -18
- package/lib/commands/jobs.js.map +1 -1
- package/lib/commands/model.d.ts +3 -0
- package/lib/commands/model.js +197 -120
- package/lib/commands/model.js.map +1 -1
- package/lib/commands/open-external.d.ts +2 -0
- package/lib/commands/open-external.js +19 -0
- package/lib/commands/open-external.js.map +1 -0
- package/lib/commands/permission.js +65 -54
- package/lib/commands/permission.js.map +1 -1
- package/lib/commands/plugins.js +381 -139
- package/lib/commands/plugins.js.map +1 -1
- package/lib/commands/schedule-catalog.d.ts +12 -0
- package/lib/commands/schedule-catalog.js +41 -0
- package/lib/commands/schedule-catalog.js.map +1 -0
- package/lib/commands/schedule-history.d.ts +4 -0
- package/lib/commands/schedule-history.js +62 -0
- package/lib/commands/schedule-history.js.map +1 -0
- package/lib/commands/schedule.d.ts +12 -15
- package/lib/commands/schedule.js +220 -187
- package/lib/commands/schedule.js.map +1 -1
- package/lib/commands/session-configuration.d.ts +3 -0
- package/lib/commands/session-configuration.js +62 -28
- package/lib/commands/session-configuration.js.map +1 -1
- package/lib/commands/session.js +111 -66
- package/lib/commands/session.js.map +1 -1
- package/lib/commands/terminal.js +14 -8
- package/lib/commands/terminal.js.map +1 -1
- package/lib/definition.d.ts +112 -15
- package/lib/index.d.ts +1 -0
- package/lib/index.js +1 -0
- package/lib/index.js.map +1 -1
- package/lib/input/actions.d.ts +1 -1
- package/lib/input/actions.js +5 -2
- package/lib/input/actions.js.map +1 -1
- package/lib/input/composer-files.d.ts +2 -0
- package/lib/input/composer-files.js +13 -3
- package/lib/input/composer-files.js.map +1 -1
- package/lib/input/image-paste.d.ts +5 -12
- package/lib/input/image-paste.js +32 -28
- package/lib/input/image-paste.js.map +1 -1
- package/lib/input/key-test.d.ts +5 -0
- package/lib/input/key-test.js +19 -0
- package/lib/input/key-test.js.map +1 -0
- package/lib/input/keys.d.ts +2 -0
- package/lib/input/keys.js +4 -1
- package/lib/input/keys.js.map +1 -1
- package/lib/locale/index.d.ts +6 -0
- package/lib/locale/index.js +32 -0
- package/lib/locale/index.js.map +1 -0
- package/lib/locale/prompt.d.ts +4 -0
- package/lib/locale/prompt.js +24 -0
- package/lib/locale/prompt.js.map +1 -0
- package/lib/locale/zh.d.ts +1114 -0
- package/lib/locale/zh.js +1115 -0
- package/lib/locale/zh.js.map +1 -0
- package/lib/prompt-intents.d.ts +4 -0
- package/lib/prompt-intents.js +4 -0
- package/lib/prompt-intents.js.map +1 -1
- package/lib/runtime/account-settings.d.ts +18 -0
- package/lib/runtime/account-settings.js +241 -0
- package/lib/runtime/account-settings.js.map +1 -0
- package/lib/runtime/agent-profile.d.ts +3 -3
- package/lib/runtime/agent-profile.js +4 -4
- package/lib/runtime/agent-profile.js.map +1 -1
- package/lib/runtime/btw-policy.js +2 -1
- package/lib/runtime/btw-policy.js.map +1 -1
- package/lib/runtime/btw.js +1 -2
- package/lib/runtime/btw.js.map +1 -1
- package/lib/runtime/composer-images.d.ts +2 -2
- package/lib/runtime/composer-images.js +4 -4
- package/lib/runtime/composer-images.js.map +1 -1
- package/lib/runtime/composer-shell.js +27 -1
- package/lib/runtime/composer-shell.js.map +1 -1
- package/lib/runtime/file-browser.d.ts +19 -0
- package/lib/runtime/file-browser.js +291 -0
- package/lib/runtime/file-browser.js.map +1 -0
- package/lib/runtime/file-review.d.ts +6 -9
- package/lib/runtime/file-review.js +91 -7
- package/lib/runtime/file-review.js.map +1 -1
- package/lib/runtime/help-reader.d.ts +12 -0
- package/lib/runtime/help-reader.js +50 -0
- package/lib/runtime/help-reader.js.map +1 -0
- package/lib/runtime/human-interaction.js +1 -1
- package/lib/runtime/human-interaction.js.map +1 -1
- package/lib/runtime/model-catalog-editor.d.ts +12 -0
- package/lib/runtime/model-catalog-editor.js +215 -0
- package/lib/runtime/model-catalog-editor.js.map +1 -0
- package/lib/runtime/model-settings.d.ts +11 -0
- package/lib/runtime/model-settings.js +404 -0
- package/lib/runtime/model-settings.js.map +1 -0
- package/lib/runtime/plugin-settings.d.ts +4 -1
- package/lib/runtime/plugin-settings.js +164 -88
- package/lib/runtime/plugin-settings.js.map +1 -1
- package/lib/runtime/prompt-browser.d.ts +9 -0
- package/lib/runtime/prompt-browser.js +19 -0
- package/lib/runtime/prompt-browser.js.map +1 -0
- package/lib/runtime/provider-local.d.ts +27 -27
- package/lib/runtime/provider-local.js +1264 -308
- package/lib/runtime/provider-local.js.map +1 -1
- package/lib/runtime/reference-reader.d.ts +15 -0
- package/lib/runtime/reference-reader.js +78 -0
- package/lib/runtime/reference-reader.js.map +1 -0
- package/lib/runtime/runner.js +38 -29
- package/lib/runtime/runner.js.map +1 -1
- package/lib/runtime/session-runtime.js +1 -1
- package/lib/runtime/session-runtime.js.map +1 -1
- package/lib/runtime/settings-draft.d.ts +21 -0
- package/lib/runtime/settings-draft.js +166 -0
- package/lib/runtime/settings-draft.js.map +1 -0
- package/lib/runtime/settings-preferences.js +1 -1
- package/lib/runtime/settings-preferences.js.map +1 -1
- package/lib/runtime/startup-notices.d.ts +0 -1
- package/lib/runtime/startup-notices.js +35 -6
- package/lib/runtime/startup-notices.js.map +1 -1
- package/lib/runtime/subagent-model-selection.js +2 -1
- package/lib/runtime/subagent-model-selection.js.map +1 -1
- package/lib/runtime/subagent-settings.d.ts +14 -0
- package/lib/runtime/subagent-settings.js +288 -0
- package/lib/runtime/subagent-settings.js.map +1 -0
- package/lib/runtime/terminal-notifications.d.ts +3 -0
- package/lib/runtime/terminal-notifications.js +13 -8
- package/lib/runtime/terminal-notifications.js.map +1 -1
- package/lib/runtime/terminal-probe.d.ts +2 -0
- package/lib/runtime/terminal-probe.js +9 -0
- package/lib/runtime/terminal-probe.js.map +1 -1
- package/lib/runtime/tool-presentation.js.map +1 -1
- package/lib/runtime/tool-reader.d.ts +5 -0
- package/lib/runtime/tool-reader.js +27 -0
- package/lib/runtime/tool-reader.js.map +1 -0
- package/lib/runtime/web-settings.d.ts +14 -0
- package/lib/runtime/web-settings.js +339 -0
- package/lib/runtime/web-settings.js.map +1 -0
- package/lib/session/command-attachments.d.ts +17 -0
- package/lib/session/command-attachments.js +50 -0
- package/lib/session/command-attachments.js.map +1 -0
- package/lib/session/context-diagnostics.d.ts +16 -0
- package/lib/session/context-diagnostics.js +15 -0
- package/lib/session/context-diagnostics.js.map +1 -1
- package/lib/session/external-agents.d.ts +3 -1
- package/lib/session/external-agents.js +35 -10
- package/lib/session/external-agents.js.map +1 -1
- package/lib/session/generation-continuation.d.ts +11 -0
- package/lib/session/generation-continuation.js +34 -0
- package/lib/session/generation-continuation.js.map +1 -0
- package/lib/session/goal-controls.d.ts +5 -0
- package/lib/session/goal-controls.js +81 -0
- package/lib/session/goal-controls.js.map +1 -0
- package/lib/session/interaction-adapter.d.ts +7 -3
- package/lib/session/interaction-adapter.js +121 -49
- package/lib/session/interaction-adapter.js.map +1 -1
- package/lib/session/message-queue.d.ts +2 -0
- package/lib/session/message-queue.js +102 -17
- package/lib/session/message-queue.js.map +1 -1
- package/lib/session/pending-inputs.d.ts +30 -0
- package/lib/session/pending-inputs.js +183 -0
- package/lib/session/pending-inputs.js.map +1 -0
- package/lib/session/session-configuration.js +1 -1
- package/lib/session/session-configuration.js.map +1 -1
- package/lib/session/session-controller.d.ts +32 -13
- package/lib/session/session-controller.js +444 -98
- package/lib/session/session-controller.js.map +1 -1
- package/lib/session/subagent-roster.d.ts +1 -0
- package/lib/session/subagent-roster.js +16 -5
- package/lib/session/subagent-roster.js.map +1 -1
- package/lib/session/subagent-tracker.d.ts +2 -0
- package/lib/session/subagent-tracker.js +18 -0
- package/lib/session/subagent-tracker.js.map +1 -1
- package/lib/session/tui-settings.d.ts +9 -5
- package/lib/session/tui-settings.js +3 -1
- package/lib/session/tui-settings.js.map +1 -1
- package/lib/session/workflow-projection.d.ts +2 -1
- package/lib/session/workflow-projection.js +34 -4
- package/lib/session/workflow-projection.js.map +1 -1
- package/lib/views/autocomplete.d.ts +10 -7
- package/lib/views/autocomplete.js +44 -37
- package/lib/views/autocomplete.js.map +1 -1
- package/lib/views/context-overview.d.ts +4 -0
- package/lib/views/context-overview.js +43 -0
- package/lib/views/context-overview.js.map +1 -0
- package/lib/views/copy-targets.d.ts +6 -2
- package/lib/views/copy-targets.js +19 -14
- package/lib/views/copy-targets.js.map +1 -1
- package/lib/views/document-reader.d.ts +10 -1
- package/lib/views/document-reader.js +59 -5
- package/lib/views/document-reader.js.map +1 -1
- package/lib/views/event-views.js +29 -11
- package/lib/views/event-views.js.map +1 -1
- package/lib/views/file-actions.js +21 -0
- package/lib/views/file-actions.js.map +1 -1
- package/lib/views/file-browser.d.ts +98 -0
- package/lib/views/file-browser.js +120 -0
- package/lib/views/file-browser.js.map +1 -0
- package/lib/views/history-search.js +4 -4
- package/lib/views/history-search.js.map +1 -1
- package/lib/views/hotkey-format.d.ts +3 -2
- package/lib/views/hotkey-format.js +4 -3
- package/lib/views/hotkey-format.js.map +1 -1
- package/lib/views/hotkeys.js +10 -12
- package/lib/views/hotkeys.js.map +1 -1
- package/lib/views/list-view.js +16 -11
- package/lib/views/list-view.js.map +1 -1
- package/lib/views/prompt-selector.d.ts +23 -1
- package/lib/views/prompt-selector.js +123 -60
- package/lib/views/prompt-selector.js.map +1 -1
- package/lib/views/settings-list.d.ts +6 -1
- package/lib/views/settings-list.js +69 -46
- package/lib/views/settings-list.js.map +1 -1
- package/lib/views/tool-actions.d.ts +16 -0
- package/lib/views/tool-actions.js +29 -0
- package/lib/views/tool-actions.js.map +1 -0
- package/lib/views/trajectory-timeline.d.ts +5 -6
- package/lib/views/trajectory-timeline.js +25 -15
- package/lib/views/trajectory-timeline.js.map +1 -1
- package/lib/views/trajectory.d.ts +23 -2
- package/lib/views/trajectory.js +404 -186
- package/lib/views/trajectory.js.map +1 -1
- package/lib/views/transcript-render.d.ts +39 -15
- package/lib/views/transcript-render.js +406 -208
- package/lib/views/transcript-render.js.map +1 -1
- package/lib/views/transcript-types.d.ts +3 -2
- package/lib/views/transcript-types.js.map +1 -1
- package/lib/views/workflow.d.ts +16 -0
- package/lib/views/workflow.js +42 -0
- package/lib/views/workflow.js.map +1 -0
- package/package.json +61 -58
- package/lib/chrome/welcome-tips.d.ts +0 -23
- package/lib/chrome/welcome-tips.js +0 -24
- package/lib/chrome/welcome-tips.js.map +0 -1
- package/lib/session/steering.d.ts +0 -3
- package/lib/session/steering.js +0 -9
- package/lib/session/steering.js.map +0 -1
|
@@ -6,16 +6,21 @@
|
|
|
6
6
|
* sessions, projections, command routing, and cleanup stay behind one API.
|
|
7
7
|
* @module @agi-fans/dsh-tui/session-controller
|
|
8
8
|
*/
|
|
9
|
+
import { openGoalControls } from "./goal-controls.js";
|
|
10
|
+
import { continuableFailure } from "./generation-continuation.js";
|
|
11
|
+
import { workflowMemberStatus, workflowOptions } from "../views/workflow.js";
|
|
9
12
|
import { randomUUID } from 'node:crypto';
|
|
10
|
-
import { steerAgent } from "./steering.js";
|
|
11
13
|
import { openMessageQueue } from "./message-queue.js";
|
|
12
14
|
import { WorkflowProjection } from "./workflow-projection.js";
|
|
13
15
|
import { join } from 'node:path';
|
|
14
16
|
import { createUserMessage, } from '@deepseek-ai/dsh-llm';
|
|
15
|
-
import {
|
|
17
|
+
import { CommandAttachments, encodeCommandAttachments } from "./command-attachments.js";
|
|
18
|
+
export { encodeCommandAttachments } from "./command-attachments.js";
|
|
19
|
+
import { isModelInvocable, isUserInvocable } from '@deepseek-ai/dsh-skill';
|
|
20
|
+
import { BUILTIN_SLASH_COMMANDS } from "../views/autocomplete.js";
|
|
16
21
|
import { readColdSessionLog } from '@deepseek-ai/dsh-session-query';
|
|
17
22
|
import { appendPluginRecord, pluginRecordOf, SessionId, SessionLogOffset } from '@deepseek-ai/dsh-session';
|
|
18
|
-
import { queueHostSubagentPrompt } from '@deepseek-ai/dsh-subagent/internal';
|
|
23
|
+
import { queueHostSubagentPrompt, steerHostSubagentPrompt } from '@deepseek-ai/dsh-subagent/internal';
|
|
19
24
|
import { firstVisibleStreamTime } from "../views/stream-time.js";
|
|
20
25
|
import { LiveAttemptTracker } from "./live-attempt-tracker.js";
|
|
21
26
|
import { blocksText } from "./content-text.js";
|
|
@@ -33,8 +38,8 @@ import { BTW_PRESET } from "../runtime/btw-policy.js";
|
|
|
33
38
|
import { reviewFileRecovery } from "../runtime/file-recovery-review.js";
|
|
34
39
|
import { isBlankSession, } from "./session-configuration.js";
|
|
35
40
|
import { stripComposerImageMarkers } from "../input/image-paste.js";
|
|
36
|
-
async function setupAgentContext(agentCtx, agent
|
|
37
|
-
const configured = await configureAgentContext(agentCtx, agent
|
|
41
|
+
async function setupAgentContext(agentCtx, agent) {
|
|
42
|
+
const configured = await configureAgentContext(agentCtx, agent);
|
|
38
43
|
await agentCtx.plugin(commandPermission(agent));
|
|
39
44
|
return configured;
|
|
40
45
|
}
|
|
@@ -45,10 +50,14 @@ function parseControl(line) {
|
|
|
45
50
|
return { name: match[1].toLowerCase(), input: match[2]?.trim() ?? '' };
|
|
46
51
|
}
|
|
47
52
|
/** Present only the session controls whose owning Harness plugins are composed. */
|
|
48
|
-
export function sessionControls(projection) {
|
|
53
|
+
export function sessionControls(projection, livePlan) {
|
|
49
54
|
const override = projection?.sandboxMode;
|
|
55
|
+
// Host pending is a target value, while the UI projection says whether a
|
|
56
|
+
// change remains. Cancelling a pending change can retain the current target.
|
|
57
|
+
const plan = livePlan === undefined ? projection?.plan
|
|
58
|
+
: { active: livePlan.active, pending: livePlan.pending !== undefined && livePlan.pending !== livePlan.active };
|
|
50
59
|
return {
|
|
51
|
-
...(
|
|
60
|
+
...(plan === undefined ? {} : { plan: { ...plan } }),
|
|
52
61
|
// A logged sandbox override (for example an approved widening) is the mode
|
|
53
62
|
// actually in force; the preset only describes the user's standing choice.
|
|
54
63
|
...(override == null
|
|
@@ -286,15 +295,13 @@ function recentSessionStatus(events) {
|
|
|
286
295
|
return 'interrupted';
|
|
287
296
|
}
|
|
288
297
|
/** Convert the human-visible part of a skill catalog into slash commands. */
|
|
289
|
-
export function userSkillCommands(skills) {
|
|
290
|
-
return skills.filter(isUserInvocable).map(skill => ({
|
|
291
|
-
name:
|
|
298
|
+
export function userSkillCommands(skills, reserved = new Set()) {
|
|
299
|
+
return skills.filter(skill => isUserInvocable(skill) && !reserved.has(skill.name)).map(skill => ({
|
|
300
|
+
name: skill.name,
|
|
292
301
|
description: compactDescription(skill.description),
|
|
302
|
+
skill: { userOnly: !isModelInvocable(skill) },
|
|
293
303
|
}));
|
|
294
304
|
}
|
|
295
|
-
function skillNameFromCommand(name) {
|
|
296
|
-
return name.startsWith('skill:') ? name.slice('skill:'.length) : name;
|
|
297
|
-
}
|
|
298
305
|
function compactDescription(value, maxLength = 140) {
|
|
299
306
|
const normalized = value.replace(/\s+/gu, ' ').trim();
|
|
300
307
|
return normalized.length <= maxLength ? normalized : normalized.slice(0, maxLength - 1).trimEnd() + '…';
|
|
@@ -329,9 +336,16 @@ export async function createSubmissionMessage(submission, attachments) {
|
|
|
329
336
|
content.push({ type: 'text', text: segment.text });
|
|
330
337
|
continue;
|
|
331
338
|
}
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
339
|
+
if (segment.kind === 'file') {
|
|
340
|
+
const attachment = submission.files?.[segment.index];
|
|
341
|
+
if (attachment !== undefined)
|
|
342
|
+
content.push({ type: 'file', attachment });
|
|
343
|
+
}
|
|
344
|
+
else {
|
|
345
|
+
const attachment = refs[segment.index];
|
|
346
|
+
if (attachment !== undefined)
|
|
347
|
+
content.push({ type: 'image', attachment });
|
|
348
|
+
}
|
|
335
349
|
}
|
|
336
350
|
// An attachment the composer did not mark in the text still belongs to the
|
|
337
351
|
// message: keep it rather than dropping it with the marker.
|
|
@@ -340,22 +354,16 @@ export async function createSubmissionMessage(submission, attachments) {
|
|
|
340
354
|
if (!placedImages.has(index))
|
|
341
355
|
content.push({ type: 'image', attachment });
|
|
342
356
|
}
|
|
343
|
-
|
|
344
|
-
|
|
357
|
+
const placedFiles = new Set(submission.segments.flatMap(segment => segment.kind === 'file' ? [segment.index] : []));
|
|
358
|
+
for (const [index, attachment] of (submission.files ?? []).entries()) {
|
|
359
|
+
if (!placedFiles.has(index))
|
|
360
|
+
content.push({ type: 'file', attachment });
|
|
361
|
+
}
|
|
345
362
|
}
|
|
346
363
|
if (content.length === 0)
|
|
347
364
|
throw new Error('Cannot submit an empty message.');
|
|
348
365
|
return createUserMessage({ content, source: { kind: 'user' } });
|
|
349
366
|
}
|
|
350
|
-
/** Encode composer drafts for `ctx.commands.execute`. */
|
|
351
|
-
export function encodeCommandAttachments(images) {
|
|
352
|
-
return images.map(image => ({
|
|
353
|
-
type: 'image',
|
|
354
|
-
mediaType: image.mediaType,
|
|
355
|
-
data: Buffer.from(image.data).toString('base64'),
|
|
356
|
-
...(image.name === undefined ? {} : { name: image.name }),
|
|
357
|
-
}));
|
|
358
|
-
}
|
|
359
367
|
/** Rehydrate one durable human inbox message into an editable composer draft. */
|
|
360
368
|
export async function restoreSubmissionMessage(message, attachments) {
|
|
361
369
|
const text = blocksText(message.content);
|
|
@@ -378,17 +386,36 @@ export async function restoreSubmissionMessage(message, attachments) {
|
|
|
378
386
|
};
|
|
379
387
|
}));
|
|
380
388
|
const files = message.content.filter((block) => block.type === 'file').map(block => block.attachment);
|
|
381
|
-
|
|
389
|
+
let imageIndex = 0, fileIndex = 0;
|
|
390
|
+
const segments = message.content.flatMap(block => {
|
|
391
|
+
if (block.type === 'text')
|
|
392
|
+
return [{ kind: 'text', text: block.text }];
|
|
393
|
+
if (block.type === 'image')
|
|
394
|
+
return [{ kind: 'image', index: imageIndex++ }];
|
|
395
|
+
if (block.type === 'file')
|
|
396
|
+
return [{ kind: 'file', index: fileIndex++ }];
|
|
397
|
+
return [];
|
|
398
|
+
});
|
|
399
|
+
return { text, images, ...(files.length === 0 ? {} : { files }),
|
|
400
|
+
...(images.length === 0 && files.length === 0 ? {} : { segments }) };
|
|
401
|
+
}
|
|
402
|
+
/** The live selection succeeded, but the profile default still needs saving. */
|
|
403
|
+
export class DefaultModelSaveError extends Error {
|
|
404
|
+
constructor(selection, cause) {
|
|
405
|
+
super(`Session model is now ${selection.provider}/${selection.model}. The default model could not be saved.`, { cause });
|
|
406
|
+
this.name = 'DefaultModelSaveError';
|
|
407
|
+
}
|
|
382
408
|
}
|
|
383
409
|
/** Own one switchable top-level Agent and project it onto a TuiService. */
|
|
384
410
|
export class SessionRuntime {
|
|
411
|
+
#commandAttachments;
|
|
385
412
|
#workflows = new WorkflowProjection();
|
|
386
413
|
#ctx;
|
|
387
414
|
#tui;
|
|
388
415
|
#active;
|
|
389
416
|
#recent = [];
|
|
390
417
|
#archivedSessions = new Set();
|
|
391
|
-
#
|
|
418
|
+
#skills = [];
|
|
392
419
|
/**
|
|
393
420
|
* Live argument vocabularies, keyed by command.
|
|
394
421
|
*
|
|
@@ -403,6 +430,7 @@ export class SessionRuntime {
|
|
|
403
430
|
#releasedHandles = new WeakSet();
|
|
404
431
|
#disposed = false;
|
|
405
432
|
#queueAbort;
|
|
433
|
+
#workflowAbort;
|
|
406
434
|
#off = [];
|
|
407
435
|
#tracker;
|
|
408
436
|
#streamAttempts = new LiveAttemptTracker();
|
|
@@ -413,11 +441,21 @@ export class SessionRuntime {
|
|
|
413
441
|
#refreshInFlight = null;
|
|
414
442
|
#refreshDirty = false;
|
|
415
443
|
#inspectEpoch = 0;
|
|
444
|
+
#inspectDeliveryTail = Promise.resolve();
|
|
445
|
+
#activeInteraction = new AbortController();
|
|
446
|
+
#pendingSends = new Map();
|
|
447
|
+
#inspectDeliveries = new Set();
|
|
448
|
+
#descendantApprovals = new Map();
|
|
416
449
|
#activationEpoch = 0;
|
|
417
450
|
#inspectedId;
|
|
418
451
|
constructor(ctx, tui, options = {}) {
|
|
419
452
|
this.#ctx = ctx;
|
|
420
453
|
this.#tui = tui;
|
|
454
|
+
const commands = ctx.get('commands');
|
|
455
|
+
if (commands !== undefined) {
|
|
456
|
+
this.#commandAttachments = new CommandAttachments(commands);
|
|
457
|
+
this.#off.push(() => this.#commandAttachments?.dispose());
|
|
458
|
+
}
|
|
421
459
|
this.#libraryPath = sessionLibraryPath(options.stateDir);
|
|
422
460
|
this.#recentRowsPath = options.stateDir === undefined ? undefined : join(options.stateDir, 'recent-sessions.json');
|
|
423
461
|
this.#tracker = new SubagentTracker({
|
|
@@ -436,9 +474,24 @@ export class SessionRuntime {
|
|
|
436
474
|
this.#tui.setInspectedSubagent(this.#inspectView(this.#inspectedId));
|
|
437
475
|
},
|
|
438
476
|
});
|
|
439
|
-
|
|
477
|
+
const offGoal = tui.setGoalHandler?.(signal => openGoalControls(ctx, tui, this.#requiredAgent(), AbortSignal.any([signal, this.interactionSignal])));
|
|
478
|
+
if (offGoal !== undefined)
|
|
479
|
+
this.#off.push(offGoal);
|
|
440
480
|
this.#off.push(tui.setQueueHandler(signal => this.openQueue(signal)));
|
|
481
|
+
const offContinue = tui.setContinueHandler?.(async (request) => {
|
|
482
|
+
const agent = this.#requiredAgent();
|
|
483
|
+
if (agent.id !== request.sessionId)
|
|
484
|
+
throw new Error('The continuation belongs to another session.');
|
|
485
|
+
await this.continueGeneration(agent, request.failureSeq);
|
|
486
|
+
});
|
|
487
|
+
if (offContinue !== undefined)
|
|
488
|
+
this.#off.push(offContinue);
|
|
441
489
|
this.#off.push(tui.onInspectSubagent(id => { void this.#inspectSubagent(id); }));
|
|
490
|
+
const offWorkflow = tui.onInspectWorkflow?.(id => {
|
|
491
|
+
void this.openWorkflow(id).catch(error => { this.#tui.notice(error instanceof Error ? error.message : String(error), { level: 'error' }); });
|
|
492
|
+
});
|
|
493
|
+
if (offWorkflow !== undefined)
|
|
494
|
+
this.#off.push(offWorkflow);
|
|
442
495
|
this.#off.push(tui.onInspectClose(() => { this.#closeInspect(); }));
|
|
443
496
|
this.#off.push(tui.onInspectSubmit(submission => { void this.#steerInspected(submission); }));
|
|
444
497
|
tui.setSessionSearch(async (query, signal) => {
|
|
@@ -489,6 +542,7 @@ export class SessionRuntime {
|
|
|
489
542
|
this.#off.push(ctx.on('agent/status', (payload) => {
|
|
490
543
|
if (payload.agent === this.#active?.handle.agent) {
|
|
491
544
|
tui.setStatus(payload.status, { root: true });
|
|
545
|
+
this.#pushSessionInfo();
|
|
492
546
|
return;
|
|
493
547
|
}
|
|
494
548
|
if (payload.agent.id === this.#inspectedId)
|
|
@@ -560,6 +614,10 @@ export class SessionRuntime {
|
|
|
560
614
|
if (delta !== undefined)
|
|
561
615
|
this.#forwardLiveDelta(payload.agent, delta);
|
|
562
616
|
}, { global: true }));
|
|
617
|
+
this.#off.push(ctx.on('goal/activation-changed', payload => {
|
|
618
|
+
if (payload.sessionId === this.#active?.handle.agent.session.id)
|
|
619
|
+
this.#pushSessionInfo();
|
|
620
|
+
}));
|
|
563
621
|
if (ctx.get('commands') !== undefined) {
|
|
564
622
|
this.#off.push(ctx.on('commands/change', () => { this.#pushCommands(); }));
|
|
565
623
|
}
|
|
@@ -585,6 +643,8 @@ export class SessionRuntime {
|
|
|
585
643
|
}));
|
|
586
644
|
}
|
|
587
645
|
}
|
|
646
|
+
/** Presentation requests from a replaced root must not survive into its successor. */
|
|
647
|
+
get interactionSignal() { return this.#activeInteraction.signal; }
|
|
588
648
|
get agent() {
|
|
589
649
|
return this.#active?.handle.agent;
|
|
590
650
|
}
|
|
@@ -597,10 +657,48 @@ export class SessionRuntime {
|
|
|
597
657
|
return false;
|
|
598
658
|
const root = this.#active?.handle.agent;
|
|
599
659
|
if (root !== undefined) {
|
|
600
|
-
|
|
660
|
+
try {
|
|
661
|
+
this.#ctx.get('subagents')?.interrupt(SessionId(this.#inspectedId), { kind: 'ancestor', agent: root });
|
|
662
|
+
}
|
|
663
|
+
catch (error) {
|
|
664
|
+
this.#tui.notice(error instanceof Error ? error.message : String(error), { level: 'error' });
|
|
665
|
+
}
|
|
601
666
|
}
|
|
602
667
|
return true;
|
|
603
668
|
}
|
|
669
|
+
#isOwnedDescendant(agent, root) {
|
|
670
|
+
const agents = this.#ctx.get('agents');
|
|
671
|
+
if (agents?.get(agent.id) !== agent
|
|
672
|
+
|| descendantDepth(agent.session, root.id, this.#lookupSession) === undefined)
|
|
673
|
+
return false;
|
|
674
|
+
// Durable lineage alone also matches an independently resumed runtime root.
|
|
675
|
+
let child = agent;
|
|
676
|
+
while (child !== root) {
|
|
677
|
+
const parentId = child.session.header.parentSession;
|
|
678
|
+
const parent = parentId === undefined ? undefined : agents.get(parentId);
|
|
679
|
+
if (parent === undefined || !agents.isOwnedBy(child.id, parent))
|
|
680
|
+
return false;
|
|
681
|
+
child = parent;
|
|
682
|
+
}
|
|
683
|
+
return true;
|
|
684
|
+
}
|
|
685
|
+
/** Accept only live descendants of the active root for shared approval prompts. */
|
|
686
|
+
beginSubagentApproval(agent) {
|
|
687
|
+
const root = this.agent;
|
|
688
|
+
if (root === undefined || !this.#isOwnedDescendant(agent, root))
|
|
689
|
+
return undefined;
|
|
690
|
+
this.#tracker.noteSession(agent.session);
|
|
691
|
+
const abort = new AbortController();
|
|
692
|
+
this.#descendantApprovals.set(abort, root);
|
|
693
|
+
const finish = this.#tracker.beginApprovalRequest(agent.id);
|
|
694
|
+
const off = this.#ctx.on('agent/disposed', event => { if (event.agent === agent)
|
|
695
|
+
abort.abort(); });
|
|
696
|
+
return { label: this.#inspectView(agent.id).label, signal: abort.signal, finish: () => {
|
|
697
|
+
off();
|
|
698
|
+
this.#descendantApprovals.delete(abort);
|
|
699
|
+
finish();
|
|
700
|
+
} };
|
|
701
|
+
}
|
|
604
702
|
async start() {
|
|
605
703
|
if (this.#started)
|
|
606
704
|
return;
|
|
@@ -611,77 +709,266 @@ export class SessionRuntime {
|
|
|
611
709
|
await this.refreshRecent();
|
|
612
710
|
await this.#activate(await this.#create(defaults), 'initial');
|
|
613
711
|
}
|
|
614
|
-
/**
|
|
712
|
+
/** Admit a complete composer value through the selected Harness inbox entry. */
|
|
615
713
|
async send(input, agent = this.#requiredAgent()) {
|
|
616
|
-
this.assertActive(agent);
|
|
617
714
|
const submission = typeof input === 'string' ? { text: input, images: [] } : input;
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
715
|
+
await this.#sendSubmission(submission, agent);
|
|
716
|
+
}
|
|
717
|
+
async #sendSubmission(submission, agent, signal) {
|
|
718
|
+
const assertDelivery = () => {
|
|
719
|
+
signal?.throwIfAborted();
|
|
720
|
+
this.assertActive(agent);
|
|
721
|
+
if (submission.delivery === 'steer' && agent.status !== 'running') {
|
|
722
|
+
throw new Error('Steering needs an active turn. Use Enter to send the next message.');
|
|
723
|
+
}
|
|
724
|
+
};
|
|
725
|
+
assertDelivery();
|
|
726
|
+
this.#pendingSends.set(agent, (this.#pendingSends.get(agent) ?? 0) + 1);
|
|
727
|
+
try {
|
|
728
|
+
const message = await createSubmissionMessage(submission, this.#ctx.get('attachments'));
|
|
729
|
+
assertDelivery();
|
|
730
|
+
this.#tui.pendingInputs?.prepare?.(submission, message.id);
|
|
731
|
+
this.#admit(agent, message, submission.delivery);
|
|
732
|
+
}
|
|
733
|
+
finally {
|
|
734
|
+
const remaining = (this.#pendingSends.get(agent) ?? 1) - 1;
|
|
735
|
+
if (remaining === 0)
|
|
736
|
+
this.#pendingSends.delete(agent);
|
|
737
|
+
else
|
|
738
|
+
this.#pendingSends.set(agent, remaining);
|
|
739
|
+
}
|
|
621
740
|
}
|
|
622
|
-
|
|
741
|
+
#admit(agent, message, delivery) {
|
|
742
|
+
const offset = agent.session.snapshotEvents().length;
|
|
743
|
+
try {
|
|
744
|
+
if (delivery === 'steer')
|
|
745
|
+
agent.steer(message);
|
|
746
|
+
else
|
|
747
|
+
agent.followup(message);
|
|
748
|
+
}
|
|
749
|
+
catch (error) {
|
|
750
|
+
// An event subscriber may throw after admission, even after the message was claimed.
|
|
751
|
+
// Only a pre-admission failure may return the submission to the composer.
|
|
752
|
+
const accepted = agent.session.snapshotEvents().slice(offset).some(event => event.type === 'agent/inbox/spliced' && event.data.inserted.some(item => item.id === message.id));
|
|
753
|
+
if (!accepted)
|
|
754
|
+
throw error;
|
|
755
|
+
this.#tui.notice(error instanceof Error ? error.message : String(error), { level: 'error' });
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
#assertCanRepeat(agent, signal) {
|
|
759
|
+
signal?.throwIfAborted();
|
|
623
760
|
this.assertActive(agent);
|
|
624
761
|
if (this.#inspectedId !== undefined)
|
|
625
|
-
throw new Error('Return to the parent session to
|
|
626
|
-
if (
|
|
762
|
+
throw new Error('Return to the parent session to continue or resend a prompt.');
|
|
763
|
+
if (agent.status !== 'idle' || this.#pendingSends.has(agent))
|
|
764
|
+
throw new Error('Wait for the current work to finish before continuing or resending.');
|
|
765
|
+
if (agent.inbox.nextTurn.length > 0 || agent.inbox.nextStep.some(message => message.source.kind === 'user')
|
|
766
|
+
|| (this.#tui.pendingInputs?.list().length ?? 0) > 0)
|
|
767
|
+
throw new Error('Resolve pending input in /queue before continuing or resending.');
|
|
768
|
+
if (this.#descendantApprovals.size > 0 || this.#ctx.get('sessionProjections')?.snapshot(agent.session).values.userQuestions?.active.some(question => question.state !== 'continued')) {
|
|
769
|
+
throw new Error('Answer the pending question or approval before continuing or resending.');
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
/** Wake an eligible failed generation with empty retry input; never duplicate the original prompt. */
|
|
773
|
+
async continueGeneration(agent = this.#requiredAgent(), expectedFailure, signal) {
|
|
774
|
+
this.#assertCanRepeat(agent, signal);
|
|
775
|
+
await agent.runMaintenance(async (maintenance) => {
|
|
776
|
+
maintenance.throwIfAborted();
|
|
777
|
+
this.#assertCanRepeat(agent, signal);
|
|
778
|
+
const failure = continuableFailure(agent.session.snapshotEvents());
|
|
779
|
+
if (failure === undefined || expectedFailure !== undefined && expectedFailure !== failure) {
|
|
780
|
+
throw new Error('No recoverable model failure is available to continue. Use /retry to resend the last prompt.');
|
|
781
|
+
}
|
|
782
|
+
this.#admit(agent, createUserMessage({ content: [], source: { kind: 'retry' } }));
|
|
783
|
+
});
|
|
784
|
+
}
|
|
785
|
+
/** Explicitly resend the complete latest human message with a fresh Harness identity. */
|
|
786
|
+
async retryLastPrompt(agent = this.#requiredAgent(), signal) {
|
|
787
|
+
this.#assertCanRepeat(agent, signal);
|
|
788
|
+
return agent.runMaintenance(async (maintenance) => {
|
|
789
|
+
maintenance.throwIfAborted();
|
|
790
|
+
this.#assertCanRepeat(agent, signal);
|
|
791
|
+
const previous = agent.session.snapshotEvents().findLast(event => event.type === 'user/message'
|
|
792
|
+
&& event.data.source.kind === 'user' && event.data.content.length > 0);
|
|
793
|
+
if (previous?.type !== 'user/message')
|
|
794
|
+
return false;
|
|
795
|
+
this.#admit(agent, createUserMessage({ content: previous.data.content, source: { kind: 'user' } }));
|
|
796
|
+
return true;
|
|
797
|
+
});
|
|
798
|
+
}
|
|
799
|
+
/** Resolve one visible live inbox, retaining its identity across asynchronous UI work. */
|
|
800
|
+
#queueTarget() {
|
|
801
|
+
const root = this.#requiredAgent();
|
|
802
|
+
const inspectedId = this.#inspectedId;
|
|
803
|
+
if (inspectedId !== undefined && !this.#inspectView(inspectedId).writable)
|
|
804
|
+
throw new Error('This subagent is read-only.');
|
|
805
|
+
const agent = inspectedId === undefined ? root : this.#ctx.get('agents')?.get(SessionId(inspectedId));
|
|
806
|
+
if (agent === undefined)
|
|
807
|
+
throw new Error('This subagent is not loaded locally. Send it a message to continue before managing its queue.');
|
|
808
|
+
const check = () => {
|
|
809
|
+
this.assertActive(root);
|
|
810
|
+
if (this.#inspectedId !== inspectedId || inspectedId !== undefined && !this.#isOwnedDescendant(agent, root)) {
|
|
811
|
+
throw new Error('The queue session is no longer active.');
|
|
812
|
+
}
|
|
813
|
+
};
|
|
814
|
+
check();
|
|
815
|
+
return { agent, check, ...(inspectedId === undefined ? {} : { label: this.#inspectView(inspectedId).label }) };
|
|
816
|
+
}
|
|
817
|
+
async openQueue(signal, requestingAgent) {
|
|
818
|
+
if (requestingAgent !== undefined)
|
|
819
|
+
this.assertActive(requestingAgent);
|
|
820
|
+
const target = this.#queueTarget();
|
|
821
|
+
const { agent } = target;
|
|
822
|
+
if (this.#queueAbort !== undefined && !this.#queueAbort.signal.aborted)
|
|
627
823
|
throw new Error('Message Queue is already open.');
|
|
824
|
+
// Validate recall state before opening; child views must never list parent delivery entries.
|
|
825
|
+
this.#tui.pendingInputs.list();
|
|
628
826
|
const abort = new AbortController();
|
|
629
827
|
this.#queueAbort = abort;
|
|
630
828
|
const onAbort = () => { abort.abort(signal.reason); };
|
|
631
829
|
signal.addEventListener('abort', onAbort, { once: true });
|
|
632
830
|
if (signal.aborted)
|
|
633
831
|
onAbort();
|
|
832
|
+
const off = this.#ctx.on('agent/disposed', event => { if (event.agent === agent)
|
|
833
|
+
abort.abort(); });
|
|
634
834
|
try {
|
|
635
|
-
await openMessageQueue({ agent,
|
|
835
|
+
await openMessageQueue({ agent, signal: abort.signal, assertActive: target.check,
|
|
836
|
+
...(target.label === undefined ? {} : { ownerLabel: target.label }),
|
|
837
|
+
tui: {
|
|
838
|
+
prompt: request => this.#tui.prompt(request), notice: (text, options) => this.#tui.notice(text, options),
|
|
839
|
+
restoreInput: submission => this.#tui.restoreInput(submission, agent.id),
|
|
840
|
+
pendingInputs: target.label === undefined ? this.#tui.pendingInputs : { list: () => [], update: () => false },
|
|
841
|
+
},
|
|
636
842
|
restore: message => restoreSubmissionMessage(message, this.#ctx.get('attachments')) });
|
|
637
843
|
}
|
|
844
|
+
catch (error) {
|
|
845
|
+
if (!abort.signal.aborted)
|
|
846
|
+
throw error;
|
|
847
|
+
}
|
|
638
848
|
finally {
|
|
849
|
+
off();
|
|
639
850
|
signal.removeEventListener('abort', onAbort);
|
|
640
851
|
if (this.#queueAbort === abort)
|
|
641
852
|
this.#queueAbort = undefined;
|
|
642
853
|
}
|
|
643
854
|
}
|
|
644
|
-
/**
|
|
645
|
-
async
|
|
646
|
-
this
|
|
855
|
+
/** Inspect durable membership; only a currently running, owned local member can be opened. */
|
|
856
|
+
async openWorkflow(id) {
|
|
857
|
+
const root = this.#requiredAgent();
|
|
858
|
+
const inspectedId = this.#inspectedId;
|
|
859
|
+
this.#workflowAbort?.abort();
|
|
860
|
+
const abort = new AbortController();
|
|
861
|
+
this.#workflowAbort = abort;
|
|
862
|
+
const check = () => {
|
|
863
|
+
abort.signal.throwIfAborted();
|
|
864
|
+
this.assertActive(root);
|
|
865
|
+
if (this.#inspectedId !== inspectedId)
|
|
866
|
+
throw new Error('The workflow session is no longer active.');
|
|
867
|
+
};
|
|
868
|
+
const canOpen = (run, member) => {
|
|
869
|
+
const child = this.#ctx.get('agents')?.get(SessionId(member.childId));
|
|
870
|
+
return workflowMemberStatus(run, member) === 'open' && child?.status === 'running'
|
|
871
|
+
&& child.session.header.parentSession === root.id && this.#isOwnedDescendant(child, root) && this.#tracker.owns(child.id);
|
|
872
|
+
};
|
|
873
|
+
try {
|
|
874
|
+
let lastRun;
|
|
875
|
+
let lastOpen = '';
|
|
876
|
+
let options = [];
|
|
877
|
+
const refresh = () => {
|
|
878
|
+
check();
|
|
879
|
+
const run = this.#workflows.get(id);
|
|
880
|
+
if (run === undefined)
|
|
881
|
+
throw new Error('This workflow is no longer available.');
|
|
882
|
+
const open = run.members.filter(member => canOpen(run, member)).map(member => member.seq).join(',');
|
|
883
|
+
if (run !== lastRun || open !== lastOpen) {
|
|
884
|
+
options = workflowOptions(run, member => canOpen(run, member));
|
|
885
|
+
lastRun = run;
|
|
886
|
+
lastOpen = open;
|
|
887
|
+
}
|
|
888
|
+
return options;
|
|
889
|
+
};
|
|
890
|
+
let selected;
|
|
891
|
+
while (!abort.signal.aborted) {
|
|
892
|
+
const answer = await this.#tui.prompt({ title: 'Workflow',
|
|
893
|
+
question: 'Browse phases and members. Only running local members can be opened.',
|
|
894
|
+
localize: ['title', 'question'], notify: false, options: refresh(), refreshOptions: refresh,
|
|
895
|
+
presentation: 'tree', filterable: true, allowCustom: false, submitLabel: 'open',
|
|
896
|
+
emptyText: 'No matching workflow members.', signal: abort.signal,
|
|
897
|
+
...(selected === undefined ? {} : { initialValue: selected }) });
|
|
898
|
+
if (answer === null)
|
|
899
|
+
return;
|
|
900
|
+
check();
|
|
901
|
+
selected = answer;
|
|
902
|
+
const run = this.#workflows.get(id);
|
|
903
|
+
const member = run?.members.find(member => `member:${member.seq}` === answer);
|
|
904
|
+
if (run === undefined || member === undefined || !canOpen(run, member)) {
|
|
905
|
+
this.#tui.notice('This workflow member is no longer available to open.');
|
|
906
|
+
continue;
|
|
907
|
+
}
|
|
908
|
+
await this.#inspectSubagent(member.childId);
|
|
909
|
+
return;
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
catch (error) {
|
|
913
|
+
if (!abort.signal.aborted)
|
|
914
|
+
throw error;
|
|
915
|
+
}
|
|
916
|
+
finally {
|
|
917
|
+
if (this.#workflowAbort === abort)
|
|
918
|
+
this.#workflowAbort = undefined;
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
/** Rehydrate before withdrawing so navigation or missing attachments leave the inbox intact. */
|
|
922
|
+
async editLatestFollowup(request) {
|
|
923
|
+
request?.signal.throwIfAborted();
|
|
924
|
+
const target = this.#queueTarget();
|
|
925
|
+
const { agent } = target;
|
|
926
|
+
if (request?.sessionId !== undefined && request.sessionId !== agent.id)
|
|
927
|
+
throw new Error('The queue session is no longer active.');
|
|
647
928
|
const message = agent.inbox.nextTurn.findLast(candidate => candidate.source.kind === 'user');
|
|
648
929
|
if (message === undefined)
|
|
649
930
|
return undefined;
|
|
650
931
|
const submission = await restoreSubmissionMessage(message, this.#ctx.get('attachments'));
|
|
651
|
-
|
|
652
|
-
|
|
932
|
+
request?.signal.throwIfAborted();
|
|
933
|
+
target.check();
|
|
934
|
+
return agent.inbox.remove(message.id) ? { ...submission, literal: true, delivery: 'queue' } : undefined;
|
|
653
935
|
}
|
|
654
936
|
/** Execute a plugin-owned slash command, falling back to user-invocable skills. */
|
|
655
|
-
async execute(
|
|
937
|
+
async execute(input, signal, images = []) {
|
|
938
|
+
const submission = typeof input === 'string' ? { text: input, images } : input;
|
|
939
|
+
const { text: line } = submission;
|
|
940
|
+
images = submission.images;
|
|
656
941
|
const commandLine = stripComposerImageMarkers(line, images);
|
|
657
942
|
const parsed = parseControl(commandLine);
|
|
658
943
|
if (parsed === undefined)
|
|
659
944
|
return false;
|
|
660
945
|
const commands = this.#ctx.get('commands');
|
|
946
|
+
const agent = this.#requiredAgent();
|
|
947
|
+
const staged = this.#commandAttachments?.stage(agent, submission);
|
|
661
948
|
const execution = await (async () => {
|
|
662
949
|
try {
|
|
663
|
-
return await commands?.execute(
|
|
950
|
+
return await commands?.execute(agent, commandLine, staged?.attachments ?? encodeCommandAttachments(images), signal);
|
|
664
951
|
}
|
|
665
952
|
finally {
|
|
953
|
+
staged?.dispose();
|
|
666
954
|
await this.#disposeRetired();
|
|
667
955
|
}
|
|
668
956
|
})();
|
|
669
957
|
if (execution === undefined) {
|
|
670
|
-
const skill = await this.#findUserSkill(
|
|
958
|
+
const skill = await this.#findUserSkill(parsed.name, signal);
|
|
959
|
+
signal.throwIfAborted();
|
|
960
|
+
this.assertActive(agent);
|
|
671
961
|
if (skill === undefined)
|
|
672
962
|
return false;
|
|
673
|
-
|
|
674
|
-
this.#tui.restoreInput({ text: line, images });
|
|
675
|
-
this.#tui.notice(`/${parsed.name} does not accept image attachments`, { level: 'error' });
|
|
676
|
-
return true;
|
|
677
|
-
}
|
|
678
|
-
await this.send('/' + skill.name + (parsed.input === '' ? '' : ' ' + parsed.input));
|
|
963
|
+
await this.#sendSubmission(submission, agent, signal);
|
|
679
964
|
return true;
|
|
680
965
|
}
|
|
681
966
|
const result = execution.result;
|
|
682
|
-
if (result.kind === 'error' && images.length > 0)
|
|
683
|
-
this.#tui.restoreInput(
|
|
684
|
-
|
|
967
|
+
if (result.kind === 'error' && (images.length > 0 || submission.files?.length))
|
|
968
|
+
this.#tui.restoreInput(submission, agent.session.id);
|
|
969
|
+
const presented = result.kind === 'success' && result.sourceEventSeq !== undefined
|
|
970
|
+
&& agent.session.snapshotEvents().some(event => event.seq === result.sourceEventSeq && event.type === 'compaction/summary');
|
|
971
|
+
if (result.text !== undefined && !presented) {
|
|
685
972
|
if (result.kind === 'error')
|
|
686
973
|
this.#tui.notice(result.text, { level: 'error', focus: true });
|
|
687
974
|
else
|
|
@@ -700,10 +987,10 @@ export class SessionRuntime {
|
|
|
700
987
|
get recentSessions() {
|
|
701
988
|
return this.#recent;
|
|
702
989
|
}
|
|
703
|
-
/** Current model selection for the active Agent. */
|
|
990
|
+
/** Current model selection for the active Agent, read from its durable projection. */
|
|
704
991
|
selection(agent = this.#requiredAgent()) {
|
|
705
992
|
this.assertActive(agent);
|
|
706
|
-
const current =
|
|
993
|
+
const current = agent.model.current();
|
|
707
994
|
if (current === undefined)
|
|
708
995
|
throw new Error('active agent has no model selection');
|
|
709
996
|
return current;
|
|
@@ -712,34 +999,42 @@ export class SessionRuntime {
|
|
|
712
999
|
async changeSelection(agent, selection, info, options) {
|
|
713
1000
|
this.assertActive(agent);
|
|
714
1001
|
const active = this.#requiredActive();
|
|
715
|
-
active.selection.current = selection;
|
|
716
1002
|
const resolved = info ?? await this.#resolveModelInfo(selection);
|
|
1003
|
+
this.assertActive(agent);
|
|
1004
|
+
const current = agent.model.current();
|
|
1005
|
+
if (current?.provider !== selection.provider || current.model !== selection.model || current.reasoningEffort !== selection.reasoningEffort) {
|
|
1006
|
+
agent.model.select(selection);
|
|
1007
|
+
}
|
|
717
1008
|
active.contextWindow = resolved?.context?.contextWindow;
|
|
718
1009
|
const status = modelStatus(selection, resolved);
|
|
719
1010
|
active.reasoningEffort = status.reasoningEffort;
|
|
720
|
-
if (options?.persist !== false) {
|
|
721
|
-
await this.#ctx.get('agentDefaultModel')?.saveSelection(selection);
|
|
722
|
-
}
|
|
723
1011
|
this.#tui.setModel(status.model, status.reasoningEffort);
|
|
724
1012
|
this.#pushSessionInfo();
|
|
1013
|
+
if (options?.persist !== false) {
|
|
1014
|
+
try {
|
|
1015
|
+
await this.#ctx.get('agentDefaultModel')?.saveSelection(selection);
|
|
1016
|
+
}
|
|
1017
|
+
catch (error) {
|
|
1018
|
+
throw new DefaultModelSaveError(selection, error);
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
725
1021
|
}
|
|
726
1022
|
/** Start a new top-level session with the current model selection. */
|
|
727
|
-
async newSession(agent) {
|
|
1023
|
+
async newSession(agent, presetId) {
|
|
728
1024
|
this.assertActive(agent);
|
|
729
|
-
await this.#activate(await this.#create(this.selection(agent)), 'new');
|
|
1025
|
+
await this.#activate(await this.#create(this.selection(agent), presetId), 'new');
|
|
730
1026
|
await this.refreshRecent();
|
|
731
1027
|
}
|
|
732
1028
|
/** Replace the active top-level session with one durable session. */
|
|
733
1029
|
async resumeSession(agent, id, signal) {
|
|
734
1030
|
this.assertActive(agent);
|
|
735
1031
|
const selection = this.selection(agent);
|
|
736
|
-
const ref = { current: selection, assembled: undefined };
|
|
737
1032
|
let configured;
|
|
738
1033
|
const handle = await this.#ctx.agents.resume({
|
|
739
1034
|
resumeSessionId: SessionId(id),
|
|
740
1035
|
agentOptions: { provider: selection.provider, model: selection.model },
|
|
741
1036
|
signal,
|
|
742
|
-
setup: async (agentCtx, agent) => { configured = await setupAgentContext(agentCtx, agent
|
|
1037
|
+
setup: async (agentCtx, agent) => { configured = await setupAgentContext(agentCtx, agent); },
|
|
743
1038
|
});
|
|
744
1039
|
const configuration = configured;
|
|
745
1040
|
if (configuration === undefined) {
|
|
@@ -748,7 +1043,6 @@ export class SessionRuntime {
|
|
|
748
1043
|
}
|
|
749
1044
|
await this.#activate({
|
|
750
1045
|
handle,
|
|
751
|
-
selection: ref,
|
|
752
1046
|
contextWindow: undefined,
|
|
753
1047
|
reasoningEffort: undefined,
|
|
754
1048
|
agentPreset: configuration.agentPreset,
|
|
@@ -764,12 +1058,11 @@ export class SessionRuntime {
|
|
|
764
1058
|
return;
|
|
765
1059
|
}
|
|
766
1060
|
const selection = this.selection(agent);
|
|
767
|
-
const ref = { current: selection, assembled: undefined };
|
|
768
1061
|
const handle = await this.#ctx.agents.resume({
|
|
769
1062
|
resumeSessionId: SessionId(id),
|
|
770
1063
|
agentOptions: { provider: selection.provider, model: selection.model },
|
|
771
1064
|
signal,
|
|
772
|
-
setup: async (agentCtx, agent) => { await setupAgentContext(agentCtx, agent
|
|
1065
|
+
setup: async (agentCtx, agent) => { await setupAgentContext(agentCtx, agent); },
|
|
773
1066
|
});
|
|
774
1067
|
try {
|
|
775
1068
|
this.#ctx.sessionTitle.rename(handle.agent.session, title);
|
|
@@ -956,7 +1249,6 @@ export class SessionRuntime {
|
|
|
956
1249
|
}));
|
|
957
1250
|
this.assertActive(agent);
|
|
958
1251
|
const selection = this.selection(agent);
|
|
959
|
-
const ref = { current: selection, assembled: undefined };
|
|
960
1252
|
const active = this.#requiredActive();
|
|
961
1253
|
let configured;
|
|
962
1254
|
const handle = await this.#ctx.agents.create({
|
|
@@ -971,7 +1263,7 @@ export class SessionRuntime {
|
|
|
971
1263
|
inheritedEventCount: SessionLogOffset(selected.branchIndex),
|
|
972
1264
|
agentOptions: { provider: selection.provider, model: selection.model },
|
|
973
1265
|
signal,
|
|
974
|
-
setup: async (agentCtx, agent) => { configured = await setupAgentContext(agentCtx, agent
|
|
1266
|
+
setup: async (agentCtx, agent) => { configured = await setupAgentContext(agentCtx, agent); },
|
|
975
1267
|
});
|
|
976
1268
|
try {
|
|
977
1269
|
this.assertActive(agent);
|
|
@@ -987,7 +1279,6 @@ export class SessionRuntime {
|
|
|
987
1279
|
}
|
|
988
1280
|
await this.#activate({
|
|
989
1281
|
handle,
|
|
990
|
-
selection: ref,
|
|
991
1282
|
contextWindow: undefined,
|
|
992
1283
|
reasoningEffort: undefined,
|
|
993
1284
|
agentPreset: configuration.agentPreset,
|
|
@@ -1220,10 +1511,15 @@ export class SessionRuntime {
|
|
|
1220
1511
|
}
|
|
1221
1512
|
async dispose() {
|
|
1222
1513
|
this.#queueAbort?.abort(new Error('session runtime disposed'));
|
|
1514
|
+
this.#workflowAbort?.abort();
|
|
1515
|
+
for (const abort of this.#descendantApprovals.keys())
|
|
1516
|
+
abort.abort();
|
|
1223
1517
|
if (this.#disposed)
|
|
1224
1518
|
return;
|
|
1225
1519
|
this.#disposed = true;
|
|
1520
|
+
this.#activeInteraction.abort();
|
|
1226
1521
|
this.#activationEpoch += 1;
|
|
1522
|
+
this.#cancelInspectDeliveries();
|
|
1227
1523
|
this.#inspectedId = undefined;
|
|
1228
1524
|
this.#tracker.reset();
|
|
1229
1525
|
this.#tui.setInspectedSubagent(undefined);
|
|
@@ -1243,15 +1539,14 @@ export class SessionRuntime {
|
|
|
1243
1539
|
this.#releasedHandles.add(handle);
|
|
1244
1540
|
await this.#disposeHandles(handles);
|
|
1245
1541
|
}
|
|
1246
|
-
async #create(selection) {
|
|
1247
|
-
const
|
|
1248
|
-
const preset = await this.#ctx.agentPresets.resolve();
|
|
1542
|
+
async #create(selection, presetId) {
|
|
1543
|
+
const preset = await this.#ctx.agentPresets.resolve(presetId);
|
|
1249
1544
|
let configured;
|
|
1250
1545
|
const handle = await this.#ctx.agents.create({
|
|
1251
1546
|
sessionId: SessionId('session-' + randomUUID()),
|
|
1252
1547
|
meta: { cwd: process.cwd(), agentPreset: preset.id },
|
|
1253
1548
|
agentOptions: { provider: selection.provider, model: selection.model },
|
|
1254
|
-
setup: async (agentCtx, agent) => { configured = await setupAgentContext(agentCtx, agent
|
|
1549
|
+
setup: async (agentCtx, agent) => { configured = await setupAgentContext(agentCtx, agent); },
|
|
1255
1550
|
});
|
|
1256
1551
|
const configuration = configured;
|
|
1257
1552
|
if (configuration === undefined) {
|
|
@@ -1260,7 +1555,6 @@ export class SessionRuntime {
|
|
|
1260
1555
|
}
|
|
1261
1556
|
return {
|
|
1262
1557
|
handle,
|
|
1263
|
-
selection: ref,
|
|
1264
1558
|
contextWindow: undefined,
|
|
1265
1559
|
reasoningEffort: undefined,
|
|
1266
1560
|
agentPreset: configuration.agentPreset,
|
|
@@ -1305,6 +1599,9 @@ export class SessionRuntime {
|
|
|
1305
1599
|
const previous = this.#active;
|
|
1306
1600
|
const epoch = ++this.#activationEpoch;
|
|
1307
1601
|
this.#queueAbort?.abort(new Error('active session changed'));
|
|
1602
|
+
this.#workflowAbort?.abort();
|
|
1603
|
+
this.#activeInteraction.abort();
|
|
1604
|
+
this.#activeInteraction = new AbortController();
|
|
1308
1605
|
this.#active = next;
|
|
1309
1606
|
void this.#claimWorkspace(next);
|
|
1310
1607
|
try {
|
|
@@ -1338,6 +1635,8 @@ export class SessionRuntime {
|
|
|
1338
1635
|
// and the activation failure remains the actionable error.
|
|
1339
1636
|
try {
|
|
1340
1637
|
if (this.#activationEpoch === epoch) {
|
|
1638
|
+
this.#activeInteraction.abort();
|
|
1639
|
+
this.#activeInteraction = new AbortController();
|
|
1341
1640
|
this.#active = previous;
|
|
1342
1641
|
if (previous !== undefined) {
|
|
1343
1642
|
try {
|
|
@@ -1370,8 +1669,12 @@ export class SessionRuntime {
|
|
|
1370
1669
|
}
|
|
1371
1670
|
/** Show one active session on the terminal: transcript, tools, controls, model. */
|
|
1372
1671
|
#presentAgent(active, reason = 'open') {
|
|
1672
|
+
for (const [abort, root] of this.#descendantApprovals)
|
|
1673
|
+
if (root !== active.handle.agent)
|
|
1674
|
+
abort.abort();
|
|
1373
1675
|
this.#workflows.reset(active.handle.agent.session.snapshotEvents());
|
|
1374
1676
|
const agent = active.handle.agent;
|
|
1677
|
+
this.#cancelInspectDeliveries();
|
|
1375
1678
|
this.#inspectedId = undefined;
|
|
1376
1679
|
this.#tui.setInspectedSubagent(undefined);
|
|
1377
1680
|
this.#tui.setStatus(agent.status);
|
|
@@ -1410,8 +1713,9 @@ export class SessionRuntime {
|
|
|
1410
1713
|
...(options === undefined ? {} : { complete: (prefix) => matchArgumentOptions(options, prefix) }),
|
|
1411
1714
|
};
|
|
1412
1715
|
});
|
|
1413
|
-
const names = new Set(commands.map(command => command.name)
|
|
1414
|
-
|
|
1716
|
+
const names = new Set([...commands.map(command => command.name),
|
|
1717
|
+
...BUILTIN_SLASH_COMMANDS.flatMap(command => [command.name, ...(command.aliases ?? [])])]);
|
|
1718
|
+
for (const skill of userSkillCommands(this.#skills, names)) {
|
|
1415
1719
|
if (names.has(skill.name))
|
|
1416
1720
|
continue;
|
|
1417
1721
|
commands.push(skill);
|
|
@@ -1433,14 +1737,14 @@ export class SessionRuntime {
|
|
|
1433
1737
|
const agent = this.agent;
|
|
1434
1738
|
const skills = this.#ctx.get('skills');
|
|
1435
1739
|
if (agent === undefined || skills === undefined) {
|
|
1436
|
-
this.#
|
|
1740
|
+
this.#skills = [];
|
|
1437
1741
|
}
|
|
1438
1742
|
else {
|
|
1439
1743
|
const cwd = currentWorkingDirectory(this.#ctx, agent.session);
|
|
1440
1744
|
const list = await skills.list({ cwd, scope: agent, signal });
|
|
1441
1745
|
if (this.#disposed || this.agent !== agent || currentWorkingDirectory(this.#ctx, agent.session) !== cwd)
|
|
1442
1746
|
return;
|
|
1443
|
-
this.#
|
|
1747
|
+
this.#skills = list;
|
|
1444
1748
|
}
|
|
1445
1749
|
this.#pushCommands();
|
|
1446
1750
|
}
|
|
@@ -1487,6 +1791,7 @@ export class SessionRuntime {
|
|
|
1487
1791
|
id,
|
|
1488
1792
|
label: view?.label ?? id,
|
|
1489
1793
|
phase: view?.phase ?? fallbackPhase,
|
|
1794
|
+
needsApproval: view?.needsApproval === true,
|
|
1490
1795
|
...(mode === undefined ? {} : { mode }),
|
|
1491
1796
|
writable: isSteerableSubagent(mode),
|
|
1492
1797
|
};
|
|
@@ -1512,6 +1817,7 @@ export class SessionRuntime {
|
|
|
1512
1817
|
#replaceTranscript(agent, reason = 'refresh') {
|
|
1513
1818
|
this.#tui.setWorkingDirectory?.(currentWorkingDirectory(this.#ctx, agent.session));
|
|
1514
1819
|
const events = agent.session.snapshotEvents();
|
|
1820
|
+
this.#tui.pendingInputs?.reconcile?.(agent.id, new Set(events.flatMap(event => event.type === 'agent/inbox/spliced' ? event.data.inserted.map(message => message.id) : [])));
|
|
1515
1821
|
this.#tracker.observeCatalog(agent.session, events);
|
|
1516
1822
|
this.#tui.replaceSession(events, this.#ctx.get('tuiToolPresentation')?.session(agent, events), agent.status, reason);
|
|
1517
1823
|
this.#replayLivePrefix(agent.session.id);
|
|
@@ -1525,10 +1831,18 @@ export class SessionRuntime {
|
|
|
1525
1831
|
if (agent !== undefined)
|
|
1526
1832
|
this.#replaceTranscript(agent);
|
|
1527
1833
|
}
|
|
1834
|
+
#cancelInspectDeliveries() {
|
|
1835
|
+
for (const delivery of this.#inspectDeliveries)
|
|
1836
|
+
delivery.abort();
|
|
1837
|
+
this.#inspectDeliveries.clear();
|
|
1838
|
+
}
|
|
1528
1839
|
#closeInspect() {
|
|
1529
1840
|
if (this.#inspectedId === undefined)
|
|
1530
1841
|
return;
|
|
1531
1842
|
this.#inspectEpoch += 1;
|
|
1843
|
+
this.#queueAbort?.abort(new Error('viewed session changed'));
|
|
1844
|
+
this.#workflowAbort?.abort();
|
|
1845
|
+
this.#cancelInspectDeliveries();
|
|
1532
1846
|
this.#inspectedId = undefined;
|
|
1533
1847
|
this.#tui.setInspectedSubagent(undefined);
|
|
1534
1848
|
const agent = this.#active?.handle.agent;
|
|
@@ -1573,6 +1887,12 @@ export class SessionRuntime {
|
|
|
1573
1887
|
}
|
|
1574
1888
|
if (request !== this.#inspectEpoch)
|
|
1575
1889
|
return;
|
|
1890
|
+
if (this.#inspectedId !== id) {
|
|
1891
|
+
this.#queueAbort?.abort(new Error('viewed session changed'));
|
|
1892
|
+
this.#workflowAbort?.abort();
|
|
1893
|
+
this.#cancelInspectDeliveries();
|
|
1894
|
+
}
|
|
1895
|
+
this.#tui.pendingInputs?.reconcile?.(id, new Set(events.flatMap(event => event.type === 'agent/inbox/spliced' ? event.data.inserted.map(message => message.id) : [])));
|
|
1576
1896
|
this.#inspectedId = id;
|
|
1577
1897
|
if (cwd !== undefined)
|
|
1578
1898
|
this.#tui.setWorkingDirectory?.(cwd);
|
|
@@ -1586,12 +1906,12 @@ export class SessionRuntime {
|
|
|
1586
1906
|
const childId = this.#inspectedId;
|
|
1587
1907
|
const root = this.#active?.handle.agent;
|
|
1588
1908
|
if (childId === undefined || root === undefined) {
|
|
1589
|
-
this.#tui.restoreInput(submission);
|
|
1909
|
+
this.#tui.restoreInput(submission, childId);
|
|
1590
1910
|
return;
|
|
1591
1911
|
}
|
|
1592
1912
|
const view = this.#inspectView(childId);
|
|
1593
1913
|
if (!view.writable) {
|
|
1594
|
-
this.#tui.restoreInput(submission);
|
|
1914
|
+
this.#tui.restoreInput(submission, childId);
|
|
1595
1915
|
this.#tui.notice('This subagent is a completed run and cannot take more messages.');
|
|
1596
1916
|
return;
|
|
1597
1917
|
}
|
|
@@ -1603,34 +1923,60 @@ export class SessionRuntime {
|
|
|
1603
1923
|
: this.#ctx.get('agents')?.get(SessionId(parentId));
|
|
1604
1924
|
const subagents = this.#ctx.get('subagents');
|
|
1605
1925
|
if (parent === undefined || subagents === undefined) {
|
|
1606
|
-
this.#tui.restoreInput(submission);
|
|
1926
|
+
this.#tui.restoreInput(submission, childId);
|
|
1607
1927
|
this.#tui.notice(parent === undefined
|
|
1608
1928
|
? 'The parent of this subagent is not live, so it cannot take a follow-up.'
|
|
1609
1929
|
: 'Subagent follow-up is unavailable in this composition.');
|
|
1610
1930
|
return;
|
|
1611
1931
|
}
|
|
1932
|
+
const previous = this.#inspectDeliveryTail;
|
|
1933
|
+
let release = () => { };
|
|
1934
|
+
this.#inspectDeliveryTail = new Promise(resolve => { release = resolve; });
|
|
1935
|
+
const delivery = new AbortController();
|
|
1936
|
+
this.#inspectDeliveries.add(delivery);
|
|
1937
|
+
let cancelUpload = () => { };
|
|
1938
|
+
const cancelled = new Promise((_resolve, reject) => {
|
|
1939
|
+
cancelUpload = () => { reject(delivery.signal.reason); };
|
|
1940
|
+
delivery.signal.addEventListener('abort', cancelUpload, { once: true });
|
|
1941
|
+
});
|
|
1612
1942
|
try {
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1943
|
+
// Recalled messages can include attachments; admission must retain submission order.
|
|
1944
|
+
await Promise.race([previous, cancelled]);
|
|
1945
|
+
delivery.signal.throwIfAborted();
|
|
1946
|
+
// Attachment storage need not support cancellation; release the draft now
|
|
1947
|
+
// and ignore a later upload result before crossing Harness admission.
|
|
1948
|
+
const message = await Promise.race([createSubmissionMessage(submission, this.#ctx.get('attachments')), cancelled]);
|
|
1949
|
+
delivery.signal.removeEventListener('abort', cancelUpload);
|
|
1950
|
+
delivery.signal.throwIfAborted();
|
|
1951
|
+
if (this.#inspectedId !== childId || this.#active?.handle.agent !== root)
|
|
1952
|
+
throw new Error('The viewed session changed before delivery.');
|
|
1953
|
+
const send = submission.delivery === 'steer' ? steerHostSubagentPrompt : queueHostSubagentPrompt;
|
|
1954
|
+
const messageId = await send(subagents, parent, SessionId(childId), message.content, { kind: 'user' }, delivery.signal);
|
|
1955
|
+
this.#tui.pendingInputs?.prepare?.(submission, messageId);
|
|
1956
|
+
this.#tui.pendingInputs?.acknowledge?.(submission);
|
|
1619
1957
|
}
|
|
1620
1958
|
catch (error) {
|
|
1621
|
-
this.#tui.restoreInput(submission);
|
|
1622
|
-
|
|
1959
|
+
this.#tui.restoreInput(submission, childId);
|
|
1960
|
+
if (!delivery.signal.aborted)
|
|
1961
|
+
this.#tui.notice(error instanceof Error ? error.message : String(error), { level: 'error' });
|
|
1962
|
+
}
|
|
1963
|
+
finally {
|
|
1964
|
+
delivery.signal.removeEventListener('abort', cancelUpload);
|
|
1965
|
+
this.#inspectDeliveries.delete(delivery);
|
|
1966
|
+
release();
|
|
1623
1967
|
}
|
|
1624
1968
|
}
|
|
1625
1969
|
#sessionControls(active, projection) {
|
|
1626
|
-
const
|
|
1627
|
-
const
|
|
1970
|
+
const liveGoal = this.#ctx.get('goals')?.get(active.handle.agent);
|
|
1971
|
+
const controls = sessionControls(projection, this.#ctx.get('planMode')?.get(active.handle.agent));
|
|
1972
|
+
if (controls.goal !== undefined && liveGoal !== undefined && liveGoal.id === projection?.goal?.goal.id && liveGoal.revision === projection?.goal?.goal.revision) {
|
|
1973
|
+
controls.goal.activation = liveGoal.activation;
|
|
1974
|
+
}
|
|
1975
|
+
const failure = active.handle.agent.status === 'idle' ? continuableFailure(active.handle.agent.session.snapshotEvents()) : undefined;
|
|
1628
1976
|
return {
|
|
1629
1977
|
...controls,
|
|
1978
|
+
...(failure === undefined ? {} : { continuation: failure }),
|
|
1630
1979
|
agentPreset: active.agentPreset,
|
|
1631
|
-
...(livePlan === undefined
|
|
1632
|
-
? {}
|
|
1633
|
-
: { plan: { active: livePlan.active, pending: livePlan.pending !== undefined } }),
|
|
1634
1980
|
};
|
|
1635
1981
|
}
|
|
1636
1982
|
/** Read one client-visible snapshot. Host-only projection state is never consulted. */
|