@agi-fans/dsh-tui 0.22.0 → 0.23.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (282) hide show
  1. package/LICENSE +26 -0
  2. package/lib/CHANGELOG.md +219 -1
  3. package/lib/chrome/box.d.ts +1 -18
  4. package/lib/chrome/box.js +2 -45
  5. package/lib/chrome/box.js.map +1 -1
  6. package/lib/chrome/code-highlight.js +4 -4
  7. package/lib/chrome/code-highlight.js.map +1 -1
  8. package/lib/chrome/command-output.d.ts +0 -1
  9. package/lib/chrome/command-output.js +11 -5
  10. package/lib/chrome/command-output.js.map +1 -1
  11. package/lib/chrome/goal-bar.js +1 -1
  12. package/lib/chrome/goal-bar.js.map +1 -1
  13. package/lib/chrome/main-screen-renderer.js +4 -3
  14. package/lib/chrome/main-screen-renderer.js.map +1 -1
  15. package/lib/chrome/markdown.d.ts +3 -3
  16. package/lib/chrome/markdown.js +85 -36
  17. package/lib/chrome/markdown.js.map +1 -1
  18. package/lib/chrome/md-style.d.ts +5 -2
  19. package/lib/chrome/md-style.js +13 -4
  20. package/lib/chrome/md-style.js.map +1 -1
  21. package/lib/chrome/renderer.d.ts +37 -1
  22. package/lib/chrome/renderer.js.map +1 -1
  23. package/lib/chrome/status-line.d.ts +3 -3
  24. package/lib/chrome/status-line.js +5 -5
  25. package/lib/chrome/status-line.js.map +1 -1
  26. package/lib/chrome/theme.d.ts +8 -3
  27. package/lib/chrome/theme.js +51 -153
  28. package/lib/chrome/theme.js.map +1 -1
  29. package/lib/commands/auth-account.d.ts +9 -0
  30. package/lib/commands/auth-account.js +202 -0
  31. package/lib/commands/auth-account.js.map +1 -0
  32. package/lib/commands/auth.js +238 -414
  33. package/lib/commands/auth.js.map +1 -1
  34. package/lib/commands/diff.js +22 -16
  35. package/lib/commands/diff.js.map +1 -1
  36. package/lib/commands/export.d.ts +0 -1
  37. package/lib/commands/export.js +99 -32
  38. package/lib/commands/export.js.map +1 -1
  39. package/lib/commands/files.js +11 -9
  40. package/lib/commands/files.js.map +1 -1
  41. package/lib/commands/integrations.d.ts +12 -1
  42. package/lib/commands/integrations.js +120 -23
  43. package/lib/commands/integrations.js.map +1 -1
  44. package/lib/commands/jobs-browser.d.ts +3 -0
  45. package/lib/commands/jobs-browser.js +152 -0
  46. package/lib/commands/jobs-browser.js.map +1 -0
  47. package/lib/commands/jobs.d.ts +3 -14
  48. package/lib/commands/jobs.js +26 -18
  49. package/lib/commands/jobs.js.map +1 -1
  50. package/lib/commands/model.d.ts +3 -0
  51. package/lib/commands/model.js +212 -128
  52. package/lib/commands/model.js.map +1 -1
  53. package/lib/commands/open-external.d.ts +2 -0
  54. package/lib/commands/open-external.js +19 -0
  55. package/lib/commands/open-external.js.map +1 -0
  56. package/lib/commands/permission.js +65 -54
  57. package/lib/commands/permission.js.map +1 -1
  58. package/lib/commands/plugins.js +381 -139
  59. package/lib/commands/plugins.js.map +1 -1
  60. package/lib/commands/schedule-catalog.d.ts +12 -0
  61. package/lib/commands/schedule-catalog.js +41 -0
  62. package/lib/commands/schedule-catalog.js.map +1 -0
  63. package/lib/commands/schedule-history.d.ts +4 -0
  64. package/lib/commands/schedule-history.js +62 -0
  65. package/lib/commands/schedule-history.js.map +1 -0
  66. package/lib/commands/schedule.d.ts +12 -15
  67. package/lib/commands/schedule.js +220 -187
  68. package/lib/commands/schedule.js.map +1 -1
  69. package/lib/commands/session-configuration.d.ts +3 -0
  70. package/lib/commands/session-configuration.js +62 -28
  71. package/lib/commands/session-configuration.js.map +1 -1
  72. package/lib/commands/session.js +116 -66
  73. package/lib/commands/session.js.map +1 -1
  74. package/lib/commands/terminal.js +14 -8
  75. package/lib/commands/terminal.js.map +1 -1
  76. package/lib/definition.d.ts +115 -15
  77. package/lib/index.d.ts +1 -0
  78. package/lib/index.js +1 -0
  79. package/lib/index.js.map +1 -1
  80. package/lib/input/actions.d.ts +1 -1
  81. package/lib/input/actions.js +5 -2
  82. package/lib/input/actions.js.map +1 -1
  83. package/lib/input/composer-files.d.ts +2 -0
  84. package/lib/input/composer-files.js +13 -3
  85. package/lib/input/composer-files.js.map +1 -1
  86. package/lib/input/image-paste.d.ts +5 -12
  87. package/lib/input/image-paste.js +32 -28
  88. package/lib/input/image-paste.js.map +1 -1
  89. package/lib/input/key-test.d.ts +5 -0
  90. package/lib/input/key-test.js +19 -0
  91. package/lib/input/key-test.js.map +1 -0
  92. package/lib/input/keys.d.ts +2 -0
  93. package/lib/input/keys.js +4 -1
  94. package/lib/input/keys.js.map +1 -1
  95. package/lib/locale/index.d.ts +6 -0
  96. package/lib/locale/index.js +32 -0
  97. package/lib/locale/index.js.map +1 -0
  98. package/lib/locale/prompt.d.ts +4 -0
  99. package/lib/locale/prompt.js +24 -0
  100. package/lib/locale/prompt.js.map +1 -0
  101. package/lib/locale/zh.d.ts +1114 -0
  102. package/lib/locale/zh.js +1115 -0
  103. package/lib/locale/zh.js.map +1 -0
  104. package/lib/prompt-intents.d.ts +4 -0
  105. package/lib/prompt-intents.js +4 -0
  106. package/lib/prompt-intents.js.map +1 -1
  107. package/lib/runtime/account-settings.d.ts +18 -0
  108. package/lib/runtime/account-settings.js +241 -0
  109. package/lib/runtime/account-settings.js.map +1 -0
  110. package/lib/runtime/agent-profile.d.ts +3 -3
  111. package/lib/runtime/agent-profile.js +4 -4
  112. package/lib/runtime/agent-profile.js.map +1 -1
  113. package/lib/runtime/btw-policy.js +2 -1
  114. package/lib/runtime/btw-policy.js.map +1 -1
  115. package/lib/runtime/btw.js +1 -2
  116. package/lib/runtime/btw.js.map +1 -1
  117. package/lib/runtime/composer-images.d.ts +2 -2
  118. package/lib/runtime/composer-images.js +4 -4
  119. package/lib/runtime/composer-images.js.map +1 -1
  120. package/lib/runtime/composer-shell.js +27 -1
  121. package/lib/runtime/composer-shell.js.map +1 -1
  122. package/lib/runtime/file-browser.d.ts +19 -0
  123. package/lib/runtime/file-browser.js +291 -0
  124. package/lib/runtime/file-browser.js.map +1 -0
  125. package/lib/runtime/file-review.d.ts +6 -9
  126. package/lib/runtime/file-review.js +91 -7
  127. package/lib/runtime/file-review.js.map +1 -1
  128. package/lib/runtime/help-reader.d.ts +12 -0
  129. package/lib/runtime/help-reader.js +50 -0
  130. package/lib/runtime/help-reader.js.map +1 -0
  131. package/lib/runtime/human-interaction.js +1 -1
  132. package/lib/runtime/human-interaction.js.map +1 -1
  133. package/lib/runtime/model-catalog-editor.d.ts +12 -0
  134. package/lib/runtime/model-catalog-editor.js +215 -0
  135. package/lib/runtime/model-catalog-editor.js.map +1 -0
  136. package/lib/runtime/model-settings.d.ts +11 -0
  137. package/lib/runtime/model-settings.js +404 -0
  138. package/lib/runtime/model-settings.js.map +1 -0
  139. package/lib/runtime/plugin-settings.d.ts +4 -1
  140. package/lib/runtime/plugin-settings.js +164 -88
  141. package/lib/runtime/plugin-settings.js.map +1 -1
  142. package/lib/runtime/prompt-browser.d.ts +9 -0
  143. package/lib/runtime/prompt-browser.js +19 -0
  144. package/lib/runtime/prompt-browser.js.map +1 -0
  145. package/lib/runtime/provider-local.d.ts +27 -27
  146. package/lib/runtime/provider-local.js +1288 -308
  147. package/lib/runtime/provider-local.js.map +1 -1
  148. package/lib/runtime/reference-reader.d.ts +15 -0
  149. package/lib/runtime/reference-reader.js +78 -0
  150. package/lib/runtime/reference-reader.js.map +1 -0
  151. package/lib/runtime/runner.js +38 -29
  152. package/lib/runtime/runner.js.map +1 -1
  153. package/lib/runtime/session-runtime.js +1 -1
  154. package/lib/runtime/session-runtime.js.map +1 -1
  155. package/lib/runtime/settings-draft.d.ts +21 -0
  156. package/lib/runtime/settings-draft.js +166 -0
  157. package/lib/runtime/settings-draft.js.map +1 -0
  158. package/lib/runtime/settings-preferences.js +1 -1
  159. package/lib/runtime/settings-preferences.js.map +1 -1
  160. package/lib/runtime/startup-notices.d.ts +0 -1
  161. package/lib/runtime/startup-notices.js +35 -6
  162. package/lib/runtime/startup-notices.js.map +1 -1
  163. package/lib/runtime/subagent-model-selection.js +2 -1
  164. package/lib/runtime/subagent-model-selection.js.map +1 -1
  165. package/lib/runtime/subagent-settings.d.ts +14 -0
  166. package/lib/runtime/subagent-settings.js +288 -0
  167. package/lib/runtime/subagent-settings.js.map +1 -0
  168. package/lib/runtime/terminal-notifications.d.ts +3 -0
  169. package/lib/runtime/terminal-notifications.js +13 -8
  170. package/lib/runtime/terminal-notifications.js.map +1 -1
  171. package/lib/runtime/terminal-probe.d.ts +2 -0
  172. package/lib/runtime/terminal-probe.js +9 -0
  173. package/lib/runtime/terminal-probe.js.map +1 -1
  174. package/lib/runtime/tool-presentation.js.map +1 -1
  175. package/lib/runtime/tool-reader.d.ts +5 -0
  176. package/lib/runtime/tool-reader.js +27 -0
  177. package/lib/runtime/tool-reader.js.map +1 -0
  178. package/lib/runtime/web-settings.d.ts +14 -0
  179. package/lib/runtime/web-settings.js +339 -0
  180. package/lib/runtime/web-settings.js.map +1 -0
  181. package/lib/session/command-attachments.d.ts +17 -0
  182. package/lib/session/command-attachments.js +50 -0
  183. package/lib/session/command-attachments.js.map +1 -0
  184. package/lib/session/context-diagnostics.d.ts +16 -0
  185. package/lib/session/context-diagnostics.js +15 -0
  186. package/lib/session/context-diagnostics.js.map +1 -1
  187. package/lib/session/external-agents.d.ts +3 -1
  188. package/lib/session/external-agents.js +35 -10
  189. package/lib/session/external-agents.js.map +1 -1
  190. package/lib/session/generation-continuation.d.ts +11 -0
  191. package/lib/session/generation-continuation.js +34 -0
  192. package/lib/session/generation-continuation.js.map +1 -0
  193. package/lib/session/goal-controls.d.ts +5 -0
  194. package/lib/session/goal-controls.js +81 -0
  195. package/lib/session/goal-controls.js.map +1 -0
  196. package/lib/session/interaction-adapter.d.ts +7 -3
  197. package/lib/session/interaction-adapter.js +121 -49
  198. package/lib/session/interaction-adapter.js.map +1 -1
  199. package/lib/session/message-queue.d.ts +2 -0
  200. package/lib/session/message-queue.js +102 -17
  201. package/lib/session/message-queue.js.map +1 -1
  202. package/lib/session/pending-inputs.d.ts +30 -0
  203. package/lib/session/pending-inputs.js +183 -0
  204. package/lib/session/pending-inputs.js.map +1 -0
  205. package/lib/session/session-configuration.js +1 -1
  206. package/lib/session/session-configuration.js.map +1 -1
  207. package/lib/session/session-controller.d.ts +33 -20
  208. package/lib/session/session-controller.js +500 -129
  209. package/lib/session/session-controller.js.map +1 -1
  210. package/lib/session/session-library.d.ts +3 -1
  211. package/lib/session/session-library.js +15 -0
  212. package/lib/session/session-library.js.map +1 -1
  213. package/lib/session/subagent-roster.d.ts +1 -0
  214. package/lib/session/subagent-roster.js +16 -5
  215. package/lib/session/subagent-roster.js.map +1 -1
  216. package/lib/session/subagent-tracker.d.ts +2 -0
  217. package/lib/session/subagent-tracker.js +18 -0
  218. package/lib/session/subagent-tracker.js.map +1 -1
  219. package/lib/session/tui-settings.d.ts +9 -5
  220. package/lib/session/tui-settings.js +3 -1
  221. package/lib/session/tui-settings.js.map +1 -1
  222. package/lib/session/workflow-projection.d.ts +2 -1
  223. package/lib/session/workflow-projection.js +34 -4
  224. package/lib/session/workflow-projection.js.map +1 -1
  225. package/lib/views/autocomplete.d.ts +12 -12
  226. package/lib/views/autocomplete.js +54 -45
  227. package/lib/views/autocomplete.js.map +1 -1
  228. package/lib/views/context-overview.d.ts +4 -0
  229. package/lib/views/context-overview.js +43 -0
  230. package/lib/views/context-overview.js.map +1 -0
  231. package/lib/views/copy-targets.d.ts +6 -2
  232. package/lib/views/copy-targets.js +19 -14
  233. package/lib/views/copy-targets.js.map +1 -1
  234. package/lib/views/document-reader.d.ts +10 -1
  235. package/lib/views/document-reader.js +63 -6
  236. package/lib/views/document-reader.js.map +1 -1
  237. package/lib/views/event-views.js +29 -11
  238. package/lib/views/event-views.js.map +1 -1
  239. package/lib/views/file-actions.js +21 -0
  240. package/lib/views/file-actions.js.map +1 -1
  241. package/lib/views/file-browser.d.ts +98 -0
  242. package/lib/views/file-browser.js +120 -0
  243. package/lib/views/file-browser.js.map +1 -0
  244. package/lib/views/history-search.js +4 -4
  245. package/lib/views/history-search.js.map +1 -1
  246. package/lib/views/hotkey-format.d.ts +3 -2
  247. package/lib/views/hotkey-format.js +4 -3
  248. package/lib/views/hotkey-format.js.map +1 -1
  249. package/lib/views/hotkeys.js +10 -12
  250. package/lib/views/hotkeys.js.map +1 -1
  251. package/lib/views/list-view.js +16 -11
  252. package/lib/views/list-view.js.map +1 -1
  253. package/lib/views/prompt-selector.d.ts +23 -1
  254. package/lib/views/prompt-selector.js +123 -60
  255. package/lib/views/prompt-selector.js.map +1 -1
  256. package/lib/views/settings-list.d.ts +6 -1
  257. package/lib/views/settings-list.js +69 -46
  258. package/lib/views/settings-list.js.map +1 -1
  259. package/lib/views/tool-actions.d.ts +16 -0
  260. package/lib/views/tool-actions.js +29 -0
  261. package/lib/views/tool-actions.js.map +1 -0
  262. package/lib/views/trajectory-timeline.d.ts +5 -6
  263. package/lib/views/trajectory-timeline.js +25 -15
  264. package/lib/views/trajectory-timeline.js.map +1 -1
  265. package/lib/views/trajectory.d.ts +23 -2
  266. package/lib/views/trajectory.js +404 -186
  267. package/lib/views/trajectory.js.map +1 -1
  268. package/lib/views/transcript-render.d.ts +39 -15
  269. package/lib/views/transcript-render.js +406 -208
  270. package/lib/views/transcript-render.js.map +1 -1
  271. package/lib/views/transcript-types.d.ts +3 -2
  272. package/lib/views/transcript-types.js.map +1 -1
  273. package/lib/views/workflow.d.ts +16 -0
  274. package/lib/views/workflow.js +42 -0
  275. package/lib/views/workflow.js.map +1 -0
  276. package/package.json +61 -58
  277. package/lib/chrome/welcome-tips.d.ts +0 -23
  278. package/lib/chrome/welcome-tips.js +0 -24
  279. package/lib/chrome/welcome-tips.js.map +0 -1
  280. package/lib/session/steering.d.ts +0 -3
  281. package/lib/session/steering.js +0 -9
  282. 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 { isUserInvocable } from '@deepseek-ai/dsh-skill';
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";
@@ -24,7 +29,7 @@ import { buildSessionTree, markedSessionTree, sessionTreeFamily } from "./sessio
24
29
  export { conversationTurns } from "./conversation-turns.js";
25
30
  import { jobNoticeFor } from "./job-notice.js";
26
31
  import { descendantDepth, isSteerableSubagent } from "./subagent-roster.js";
27
- import { readRecentRows, writeRecentRows, readSessionLibrary, sessionLibraryPath, setSessionLabel } from "./session-library.js";
32
+ import { readRecentRows, writeRecentRows, readSessionLibrary, sessionArgumentOptions, sessionLibraryPath, setSessionLabel } from "./session-library.js";
28
33
  import { currentWorkingDirectory } from "./working-directory.js";
29
34
  import { SubagentTracker } from "./subagent-tracker.js";
30
35
  import { commandPermission } from "../commands/permission.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, selection) {
37
- const configured = await configureAgentContext(agentCtx, agent, selection);
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
- ...(projection?.plan === undefined ? {} : { plan: { ...projection.plan } }),
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: `skill:${skill.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
- const attachment = refs[segment.index];
333
- if (attachment !== undefined)
334
- content.push({ type: 'image', attachment });
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
- for (const attachment of submission.files ?? [])
344
- content.push({ type: 'file', attachment });
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,31 +386,52 @@ 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
- return { text, images, ...(files.length === 0 ? {} : { files }) };
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
- #skillCommands = [];
418
+ #skills = [];
392
419
  /**
393
420
  * Live argument vocabularies, keyed by command.
394
421
  *
395
422
  * A snapshot rather than a callback into the services: completion runs on every
396
- * keystroke and must not await a catalog. It is rebuilt whenever the commands
397
- * are pushed, which is also when a model or provider list can have changed.
423
+ * keystroke and must not await a catalog. Session refreshes publish recent
424
+ * rows immediately and load each provider independently.
398
425
  */
399
426
  #argumentOptions = new Map();
427
+ #argumentEpoch = 0;
400
428
  #started = false;
401
429
  #retired = [];
402
430
  // Weak: deduplication must not retain disposed session graphs.
403
431
  #releasedHandles = new WeakSet();
404
432
  #disposed = false;
405
433
  #queueAbort;
434
+ #workflowAbort;
406
435
  #off = [];
407
436
  #tracker;
408
437
  #streamAttempts = new LiveAttemptTracker();
@@ -413,11 +442,21 @@ export class SessionRuntime {
413
442
  #refreshInFlight = null;
414
443
  #refreshDirty = false;
415
444
  #inspectEpoch = 0;
445
+ #inspectDeliveryTail = Promise.resolve();
446
+ #activeInteraction = new AbortController();
447
+ #pendingSends = new Map();
448
+ #inspectDeliveries = new Set();
449
+ #descendantApprovals = new Map();
416
450
  #activationEpoch = 0;
417
451
  #inspectedId;
418
452
  constructor(ctx, tui, options = {}) {
419
453
  this.#ctx = ctx;
420
454
  this.#tui = tui;
455
+ const commands = ctx.get('commands');
456
+ if (commands !== undefined) {
457
+ this.#commandAttachments = new CommandAttachments(commands);
458
+ this.#off.push(() => this.#commandAttachments?.dispose());
459
+ }
421
460
  this.#libraryPath = sessionLibraryPath(options.stateDir);
422
461
  this.#recentRowsPath = options.stateDir === undefined ? undefined : join(options.stateDir, 'recent-sessions.json');
423
462
  this.#tracker = new SubagentTracker({
@@ -436,9 +475,24 @@ export class SessionRuntime {
436
475
  this.#tui.setInspectedSubagent(this.#inspectView(this.#inspectedId));
437
476
  },
438
477
  });
439
- this.#off.push(tui.setSteerHandler(text => { steerAgent(this.agent, text); }));
478
+ const offGoal = tui.setGoalHandler?.(signal => openGoalControls(ctx, tui, this.#requiredAgent(), AbortSignal.any([signal, this.interactionSignal])));
479
+ if (offGoal !== undefined)
480
+ this.#off.push(offGoal);
440
481
  this.#off.push(tui.setQueueHandler(signal => this.openQueue(signal)));
482
+ const offContinue = tui.setContinueHandler?.(async (request) => {
483
+ const agent = this.#requiredAgent();
484
+ if (agent.id !== request.sessionId)
485
+ throw new Error('The continuation belongs to another session.');
486
+ await this.continueGeneration(agent, request.failureSeq);
487
+ });
488
+ if (offContinue !== undefined)
489
+ this.#off.push(offContinue);
441
490
  this.#off.push(tui.onInspectSubagent(id => { void this.#inspectSubagent(id); }));
491
+ const offWorkflow = tui.onInspectWorkflow?.(id => {
492
+ void this.openWorkflow(id).catch(error => { this.#tui.notice(error instanceof Error ? error.message : String(error), { level: 'error' }); });
493
+ });
494
+ if (offWorkflow !== undefined)
495
+ this.#off.push(offWorkflow);
442
496
  this.#off.push(tui.onInspectClose(() => { this.#closeInspect(); }));
443
497
  this.#off.push(tui.onInspectSubmit(submission => { void this.#steerInspected(submission); }));
444
498
  tui.setSessionSearch(async (query, signal) => {
@@ -489,6 +543,7 @@ export class SessionRuntime {
489
543
  this.#off.push(ctx.on('agent/status', (payload) => {
490
544
  if (payload.agent === this.#active?.handle.agent) {
491
545
  tui.setStatus(payload.status, { root: true });
546
+ this.#pushSessionInfo();
492
547
  return;
493
548
  }
494
549
  if (payload.agent.id === this.#inspectedId)
@@ -512,8 +567,10 @@ export class SessionRuntime {
512
567
  if (session.id === (this.#inspectedId ?? active.handle.agent.id)) {
513
568
  tui.setWorkingDirectory?.(currentWorkingDirectory(ctx, session));
514
569
  }
515
- if (session === active.handle.agent.session)
570
+ if (session === active.handle.agent.session) {
516
571
  void this.#refreshSkills().catch(error => { ctx.logger.warn(error); });
572
+ void this.refreshRecent().catch(error => { ctx.logger.warn(error); });
573
+ }
517
574
  }
518
575
  if (session.id === this.#inspectedId) {
519
576
  const child = ctx.get('agents')?.get(session.id);
@@ -560,6 +617,10 @@ export class SessionRuntime {
560
617
  if (delta !== undefined)
561
618
  this.#forwardLiveDelta(payload.agent, delta);
562
619
  }, { global: true }));
620
+ this.#off.push(ctx.on('goal/activation-changed', payload => {
621
+ if (payload.sessionId === this.#active?.handle.agent.session.id)
622
+ this.#pushSessionInfo();
623
+ }));
563
624
  if (ctx.get('commands') !== undefined) {
564
625
  this.#off.push(ctx.on('commands/change', () => { this.#pushCommands(); }));
565
626
  }
@@ -585,6 +646,8 @@ export class SessionRuntime {
585
646
  }));
586
647
  }
587
648
  }
649
+ /** Presentation requests from a replaced root must not survive into its successor. */
650
+ get interactionSignal() { return this.#activeInteraction.signal; }
588
651
  get agent() {
589
652
  return this.#active?.handle.agent;
590
653
  }
@@ -597,10 +660,48 @@ export class SessionRuntime {
597
660
  return false;
598
661
  const root = this.#active?.handle.agent;
599
662
  if (root !== undefined) {
600
- this.#ctx.get('subagents')?.interrupt(SessionId(this.#inspectedId), { kind: 'ancestor', agent: root });
663
+ try {
664
+ this.#ctx.get('subagents')?.interrupt(SessionId(this.#inspectedId), { kind: 'ancestor', agent: root });
665
+ }
666
+ catch (error) {
667
+ this.#tui.notice(error instanceof Error ? error.message : String(error), { level: 'error' });
668
+ }
601
669
  }
602
670
  return true;
603
671
  }
672
+ #isOwnedDescendant(agent, root) {
673
+ const agents = this.#ctx.get('agents');
674
+ if (agents?.get(agent.id) !== agent
675
+ || descendantDepth(agent.session, root.id, this.#lookupSession) === undefined)
676
+ return false;
677
+ // Durable lineage alone also matches an independently resumed runtime root.
678
+ let child = agent;
679
+ while (child !== root) {
680
+ const parentId = child.session.header.parentSession;
681
+ const parent = parentId === undefined ? undefined : agents.get(parentId);
682
+ if (parent === undefined || !agents.isOwnedBy(child.id, parent))
683
+ return false;
684
+ child = parent;
685
+ }
686
+ return true;
687
+ }
688
+ /** Accept only live descendants of the active root for shared approval prompts. */
689
+ beginSubagentApproval(agent) {
690
+ const root = this.agent;
691
+ if (root === undefined || !this.#isOwnedDescendant(agent, root))
692
+ return undefined;
693
+ this.#tracker.noteSession(agent.session);
694
+ const abort = new AbortController();
695
+ this.#descendantApprovals.set(abort, root);
696
+ const finish = this.#tracker.beginApprovalRequest(agent.id);
697
+ const off = this.#ctx.on('agent/disposed', event => { if (event.agent === agent)
698
+ abort.abort(); });
699
+ return { label: this.#inspectView(agent.id).label, signal: abort.signal, finish: () => {
700
+ off();
701
+ this.#descendantApprovals.delete(abort);
702
+ finish();
703
+ } };
704
+ }
604
705
  async start() {
605
706
  if (this.#started)
606
707
  return;
@@ -611,77 +712,266 @@ export class SessionRuntime {
611
712
  await this.refreshRecent();
612
713
  await this.#activate(await this.#create(defaults), 'initial');
613
714
  }
614
- /** Submit one human composer value; active turns retain it as a later follow-up. */
715
+ /** Admit a complete composer value through the selected Harness inbox entry. */
615
716
  async send(input, agent = this.#requiredAgent()) {
616
- this.assertActive(agent);
617
717
  const submission = typeof input === 'string' ? { text: input, images: [] } : input;
618
- const message = await createSubmissionMessage(submission, this.#ctx.get('attachments'));
619
- this.assertActive(agent);
620
- agent.followup(message);
718
+ await this.#sendSubmission(submission, agent);
621
719
  }
622
- async openQueue(signal, agent = this.#requiredAgent()) {
720
+ async #sendSubmission(submission, agent, signal) {
721
+ const assertDelivery = () => {
722
+ signal?.throwIfAborted();
723
+ this.assertActive(agent);
724
+ if (submission.delivery === 'steer' && agent.status !== 'running') {
725
+ throw new Error('Steering needs an active turn. Use Enter to send the next message.');
726
+ }
727
+ };
728
+ assertDelivery();
729
+ this.#pendingSends.set(agent, (this.#pendingSends.get(agent) ?? 0) + 1);
730
+ try {
731
+ const message = await createSubmissionMessage(submission, this.#ctx.get('attachments'));
732
+ assertDelivery();
733
+ this.#tui.pendingInputs?.prepare?.(submission, message.id);
734
+ this.#admit(agent, message, submission.delivery);
735
+ }
736
+ finally {
737
+ const remaining = (this.#pendingSends.get(agent) ?? 1) - 1;
738
+ if (remaining === 0)
739
+ this.#pendingSends.delete(agent);
740
+ else
741
+ this.#pendingSends.set(agent, remaining);
742
+ }
743
+ }
744
+ #admit(agent, message, delivery) {
745
+ const offset = agent.session.snapshotEvents().length;
746
+ try {
747
+ if (delivery === 'steer')
748
+ agent.steer(message);
749
+ else
750
+ agent.followup(message);
751
+ }
752
+ catch (error) {
753
+ // An event subscriber may throw after admission, even after the message was claimed.
754
+ // Only a pre-admission failure may return the submission to the composer.
755
+ const accepted = agent.session.snapshotEvents().slice(offset).some(event => event.type === 'agent/inbox/spliced' && event.data.inserted.some(item => item.id === message.id));
756
+ if (!accepted)
757
+ throw error;
758
+ this.#tui.notice(error instanceof Error ? error.message : String(error), { level: 'error' });
759
+ }
760
+ }
761
+ #assertCanRepeat(agent, signal) {
762
+ signal?.throwIfAborted();
623
763
  this.assertActive(agent);
624
764
  if (this.#inspectedId !== undefined)
625
- throw new Error('Return to the parent session to manage queued messages.');
626
- if (this.#queueAbort !== undefined)
765
+ throw new Error('Return to the parent session to continue or resend a prompt.');
766
+ if (agent.status !== 'idle' || this.#pendingSends.has(agent))
767
+ throw new Error('Wait for the current work to finish before continuing or resending.');
768
+ if (agent.inbox.nextTurn.length > 0 || agent.inbox.nextStep.some(message => message.source.kind === 'user')
769
+ || (this.#tui.pendingInputs?.list().length ?? 0) > 0)
770
+ throw new Error('Resolve pending input in /queue before continuing or resending.');
771
+ if (this.#descendantApprovals.size > 0 || this.#ctx.get('sessionProjections')?.snapshot(agent.session).values.userQuestions?.active.some(question => question.state !== 'continued')) {
772
+ throw new Error('Answer the pending question or approval before continuing or resending.');
773
+ }
774
+ }
775
+ /** Wake an eligible failed generation with empty retry input; never duplicate the original prompt. */
776
+ async continueGeneration(agent = this.#requiredAgent(), expectedFailure, signal) {
777
+ this.#assertCanRepeat(agent, signal);
778
+ await agent.runMaintenance(async (maintenance) => {
779
+ maintenance.throwIfAborted();
780
+ this.#assertCanRepeat(agent, signal);
781
+ const failure = continuableFailure(agent.session.snapshotEvents());
782
+ if (failure === undefined || expectedFailure !== undefined && expectedFailure !== failure) {
783
+ throw new Error('No recoverable model failure is available to continue. Use /retry to resend the last prompt.');
784
+ }
785
+ this.#admit(agent, createUserMessage({ content: [], source: { kind: 'retry' } }));
786
+ });
787
+ }
788
+ /** Explicitly resend the complete latest human message with a fresh Harness identity. */
789
+ async retryLastPrompt(agent = this.#requiredAgent(), signal) {
790
+ this.#assertCanRepeat(agent, signal);
791
+ return agent.runMaintenance(async (maintenance) => {
792
+ maintenance.throwIfAborted();
793
+ this.#assertCanRepeat(agent, signal);
794
+ const previous = agent.session.snapshotEvents().findLast(event => event.type === 'user/message'
795
+ && event.data.source.kind === 'user' && event.data.content.length > 0);
796
+ if (previous?.type !== 'user/message')
797
+ return false;
798
+ this.#admit(agent, createUserMessage({ content: previous.data.content, source: { kind: 'user' } }));
799
+ return true;
800
+ });
801
+ }
802
+ /** Resolve one visible live inbox, retaining its identity across asynchronous UI work. */
803
+ #queueTarget() {
804
+ const root = this.#requiredAgent();
805
+ const inspectedId = this.#inspectedId;
806
+ if (inspectedId !== undefined && !this.#inspectView(inspectedId).writable)
807
+ throw new Error('This subagent is read-only.');
808
+ const agent = inspectedId === undefined ? root : this.#ctx.get('agents')?.get(SessionId(inspectedId));
809
+ if (agent === undefined)
810
+ throw new Error('This subagent is not loaded locally. Send it a message to continue before managing its queue.');
811
+ const check = () => {
812
+ this.assertActive(root);
813
+ if (this.#inspectedId !== inspectedId || inspectedId !== undefined && !this.#isOwnedDescendant(agent, root)) {
814
+ throw new Error('The queue session is no longer active.');
815
+ }
816
+ };
817
+ check();
818
+ return { agent, check, ...(inspectedId === undefined ? {} : { label: this.#inspectView(inspectedId).label }) };
819
+ }
820
+ async openQueue(signal, requestingAgent) {
821
+ if (requestingAgent !== undefined)
822
+ this.assertActive(requestingAgent);
823
+ const target = this.#queueTarget();
824
+ const { agent } = target;
825
+ if (this.#queueAbort !== undefined && !this.#queueAbort.signal.aborted)
627
826
  throw new Error('Message Queue is already open.');
827
+ // Validate recall state before opening; child views must never list parent delivery entries.
828
+ this.#tui.pendingInputs.list();
628
829
  const abort = new AbortController();
629
830
  this.#queueAbort = abort;
630
831
  const onAbort = () => { abort.abort(signal.reason); };
631
832
  signal.addEventListener('abort', onAbort, { once: true });
632
833
  if (signal.aborted)
633
834
  onAbort();
835
+ const off = this.#ctx.on('agent/disposed', event => { if (event.agent === agent)
836
+ abort.abort(); });
634
837
  try {
635
- await openMessageQueue({ agent, tui: this.#tui, signal: abort.signal, assertActive: () => { this.assertActive(agent); },
838
+ await openMessageQueue({ agent, signal: abort.signal, assertActive: target.check,
839
+ ...(target.label === undefined ? {} : { ownerLabel: target.label }),
840
+ tui: {
841
+ prompt: request => this.#tui.prompt(request), notice: (text, options) => this.#tui.notice(text, options),
842
+ restoreInput: submission => this.#tui.restoreInput(submission, agent.id),
843
+ pendingInputs: target.label === undefined ? this.#tui.pendingInputs : { list: () => [], update: () => false },
844
+ },
636
845
  restore: message => restoreSubmissionMessage(message, this.#ctx.get('attachments')) });
637
846
  }
847
+ catch (error) {
848
+ if (!abort.signal.aborted)
849
+ throw error;
850
+ }
638
851
  finally {
852
+ off();
639
853
  signal.removeEventListener('abort', onAbort);
640
854
  if (this.#queueAbort === abort)
641
855
  this.#queueAbort = undefined;
642
856
  }
643
857
  }
644
- /** Remove and rehydrate the newest durable human follow-up for queue browsing. */
645
- async editLatestFollowup(agent = this.#requiredAgent()) {
646
- this.assertActive(agent);
858
+ /** Inspect durable membership; only a currently running, owned local member can be opened. */
859
+ async openWorkflow(id) {
860
+ const root = this.#requiredAgent();
861
+ const inspectedId = this.#inspectedId;
862
+ this.#workflowAbort?.abort();
863
+ const abort = new AbortController();
864
+ this.#workflowAbort = abort;
865
+ const check = () => {
866
+ abort.signal.throwIfAborted();
867
+ this.assertActive(root);
868
+ if (this.#inspectedId !== inspectedId)
869
+ throw new Error('The workflow session is no longer active.');
870
+ };
871
+ const canOpen = (run, member) => {
872
+ const child = this.#ctx.get('agents')?.get(SessionId(member.childId));
873
+ return workflowMemberStatus(run, member) === 'open' && child?.status === 'running'
874
+ && child.session.header.parentSession === root.id && this.#isOwnedDescendant(child, root) && this.#tracker.owns(child.id);
875
+ };
876
+ try {
877
+ let lastRun;
878
+ let lastOpen = '';
879
+ let options = [];
880
+ const refresh = () => {
881
+ check();
882
+ const run = this.#workflows.get(id);
883
+ if (run === undefined)
884
+ throw new Error('This workflow is no longer available.');
885
+ const open = run.members.filter(member => canOpen(run, member)).map(member => member.seq).join(',');
886
+ if (run !== lastRun || open !== lastOpen) {
887
+ options = workflowOptions(run, member => canOpen(run, member));
888
+ lastRun = run;
889
+ lastOpen = open;
890
+ }
891
+ return options;
892
+ };
893
+ let selected;
894
+ while (!abort.signal.aborted) {
895
+ const answer = await this.#tui.prompt({ title: 'Workflow',
896
+ question: 'Browse phases and members. Only running local members can be opened.',
897
+ localize: ['title', 'question'], notify: false, options: refresh(), refreshOptions: refresh,
898
+ presentation: 'tree', filterable: true, allowCustom: false, submitLabel: 'open',
899
+ emptyText: 'No matching workflow members.', signal: abort.signal,
900
+ ...(selected === undefined ? {} : { initialValue: selected }) });
901
+ if (answer === null)
902
+ return;
903
+ check();
904
+ selected = answer;
905
+ const run = this.#workflows.get(id);
906
+ const member = run?.members.find(member => `member:${member.seq}` === answer);
907
+ if (run === undefined || member === undefined || !canOpen(run, member)) {
908
+ this.#tui.notice('This workflow member is no longer available to open.');
909
+ continue;
910
+ }
911
+ await this.#inspectSubagent(member.childId);
912
+ return;
913
+ }
914
+ }
915
+ catch (error) {
916
+ if (!abort.signal.aborted)
917
+ throw error;
918
+ }
919
+ finally {
920
+ if (this.#workflowAbort === abort)
921
+ this.#workflowAbort = undefined;
922
+ }
923
+ }
924
+ /** Rehydrate before withdrawing so navigation or missing attachments leave the inbox intact. */
925
+ async editLatestFollowup(request) {
926
+ request?.signal.throwIfAborted();
927
+ const target = this.#queueTarget();
928
+ const { agent } = target;
929
+ if (request?.sessionId !== undefined && request.sessionId !== agent.id)
930
+ throw new Error('The queue session is no longer active.');
647
931
  const message = agent.inbox.nextTurn.findLast(candidate => candidate.source.kind === 'user');
648
932
  if (message === undefined)
649
933
  return undefined;
650
934
  const submission = await restoreSubmissionMessage(message, this.#ctx.get('attachments'));
651
- this.assertActive(agent);
652
- return agent.inbox.remove(message.id) ? { ...submission, literal: true } : undefined;
935
+ request?.signal.throwIfAborted();
936
+ target.check();
937
+ return agent.inbox.remove(message.id) ? { ...submission, literal: true, delivery: 'queue' } : undefined;
653
938
  }
654
939
  /** Execute a plugin-owned slash command, falling back to user-invocable skills. */
655
- async execute(line, signal, images = []) {
940
+ async execute(input, signal, images = []) {
941
+ const submission = typeof input === 'string' ? { text: input, images } : input;
942
+ const { text: line } = submission;
943
+ images = submission.images;
656
944
  const commandLine = stripComposerImageMarkers(line, images);
657
945
  const parsed = parseControl(commandLine);
658
946
  if (parsed === undefined)
659
947
  return false;
660
948
  const commands = this.#ctx.get('commands');
949
+ const agent = this.#requiredAgent();
950
+ const staged = this.#commandAttachments?.stage(agent, submission);
661
951
  const execution = await (async () => {
662
952
  try {
663
- return await commands?.execute(this.#requiredAgent(), commandLine, encodeCommandAttachments(images), signal);
953
+ return await commands?.execute(agent, commandLine, staged?.attachments ?? encodeCommandAttachments(images), signal);
664
954
  }
665
955
  finally {
956
+ staged?.dispose();
666
957
  await this.#disposeRetired();
667
958
  }
668
959
  })();
669
960
  if (execution === undefined) {
670
- const skill = await this.#findUserSkill(skillNameFromCommand(parsed.name), signal);
961
+ const skill = await this.#findUserSkill(parsed.name, signal);
962
+ signal.throwIfAborted();
963
+ this.assertActive(agent);
671
964
  if (skill === undefined)
672
965
  return false;
673
- if (images.length > 0) {
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));
966
+ await this.#sendSubmission(submission, agent, signal);
679
967
  return true;
680
968
  }
681
969
  const result = execution.result;
682
- if (result.kind === 'error' && images.length > 0)
683
- this.#tui.restoreInput({ text: line, images });
684
- if (result.text !== undefined) {
970
+ if (result.kind === 'error' && (images.length > 0 || submission.files?.length))
971
+ this.#tui.restoreInput(submission, agent.session.id);
972
+ const presented = result.kind === 'success' && result.sourceEventSeq !== undefined
973
+ && agent.session.snapshotEvents().some(event => event.seq === result.sourceEventSeq && event.type === 'compaction/summary');
974
+ if (result.text !== undefined && !presented) {
685
975
  if (result.kind === 'error')
686
976
  this.#tui.notice(result.text, { level: 'error', focus: true });
687
977
  else
@@ -700,10 +990,10 @@ export class SessionRuntime {
700
990
  get recentSessions() {
701
991
  return this.#recent;
702
992
  }
703
- /** Current model selection for the active Agent. */
993
+ /** Current model selection for the active Agent, read from its durable projection. */
704
994
  selection(agent = this.#requiredAgent()) {
705
995
  this.assertActive(agent);
706
- const current = this.#requiredActive().selection.current;
996
+ const current = agent.model.current();
707
997
  if (current === undefined)
708
998
  throw new Error('active agent has no model selection');
709
999
  return current;
@@ -712,34 +1002,42 @@ export class SessionRuntime {
712
1002
  async changeSelection(agent, selection, info, options) {
713
1003
  this.assertActive(agent);
714
1004
  const active = this.#requiredActive();
715
- active.selection.current = selection;
716
1005
  const resolved = info ?? await this.#resolveModelInfo(selection);
1006
+ this.assertActive(agent);
1007
+ const current = agent.model.current();
1008
+ if (current?.provider !== selection.provider || current.model !== selection.model || current.reasoningEffort !== selection.reasoningEffort) {
1009
+ agent.model.select(selection);
1010
+ }
717
1011
  active.contextWindow = resolved?.context?.contextWindow;
718
1012
  const status = modelStatus(selection, resolved);
719
1013
  active.reasoningEffort = status.reasoningEffort;
720
- if (options?.persist !== false) {
721
- await this.#ctx.get('agentDefaultModel')?.saveSelection(selection);
722
- }
723
1014
  this.#tui.setModel(status.model, status.reasoningEffort);
724
1015
  this.#pushSessionInfo();
1016
+ if (options?.persist !== false) {
1017
+ try {
1018
+ await this.#ctx.get('agentDefaultModel')?.saveSelection(selection);
1019
+ }
1020
+ catch (error) {
1021
+ throw new DefaultModelSaveError(selection, error);
1022
+ }
1023
+ }
725
1024
  }
726
1025
  /** Start a new top-level session with the current model selection. */
727
- async newSession(agent) {
1026
+ async newSession(agent, presetId) {
728
1027
  this.assertActive(agent);
729
- await this.#activate(await this.#create(this.selection(agent)), 'new');
1028
+ await this.#activate(await this.#create(this.selection(agent), presetId), 'new');
730
1029
  await this.refreshRecent();
731
1030
  }
732
1031
  /** Replace the active top-level session with one durable session. */
733
1032
  async resumeSession(agent, id, signal) {
734
1033
  this.assertActive(agent);
735
1034
  const selection = this.selection(agent);
736
- const ref = { current: selection, assembled: undefined };
737
1035
  let configured;
738
1036
  const handle = await this.#ctx.agents.resume({
739
1037
  resumeSessionId: SessionId(id),
740
1038
  agentOptions: { provider: selection.provider, model: selection.model },
741
1039
  signal,
742
- setup: async (agentCtx, agent) => { configured = await setupAgentContext(agentCtx, agent, ref); },
1040
+ setup: async (agentCtx, agent) => { configured = await setupAgentContext(agentCtx, agent); },
743
1041
  });
744
1042
  const configuration = configured;
745
1043
  if (configuration === undefined) {
@@ -748,7 +1046,6 @@ export class SessionRuntime {
748
1046
  }
749
1047
  await this.#activate({
750
1048
  handle,
751
- selection: ref,
752
1049
  contextWindow: undefined,
753
1050
  reasoningEffort: undefined,
754
1051
  agentPreset: configuration.agentPreset,
@@ -764,12 +1061,11 @@ export class SessionRuntime {
764
1061
  return;
765
1062
  }
766
1063
  const selection = this.selection(agent);
767
- const ref = { current: selection, assembled: undefined };
768
1064
  const handle = await this.#ctx.agents.resume({
769
1065
  resumeSessionId: SessionId(id),
770
1066
  agentOptions: { provider: selection.provider, model: selection.model },
771
1067
  signal,
772
- setup: async (agentCtx, agent) => { await setupAgentContext(agentCtx, agent, ref); },
1068
+ setup: async (agentCtx, agent) => { await setupAgentContext(agentCtx, agent); },
773
1069
  });
774
1070
  try {
775
1071
  this.#ctx.sessionTitle.rename(handle.agent.session, title);
@@ -956,7 +1252,6 @@ export class SessionRuntime {
956
1252
  }));
957
1253
  this.assertActive(agent);
958
1254
  const selection = this.selection(agent);
959
- const ref = { current: selection, assembled: undefined };
960
1255
  const active = this.#requiredActive();
961
1256
  let configured;
962
1257
  const handle = await this.#ctx.agents.create({
@@ -971,7 +1266,12 @@ export class SessionRuntime {
971
1266
  inheritedEventCount: SessionLogOffset(selected.branchIndex),
972
1267
  agentOptions: { provider: selection.provider, model: selection.model },
973
1268
  signal,
974
- setup: async (agentCtx, agent) => { configured = await setupAgentContext(agentCtx, agent, ref); },
1269
+ setup: async (agentCtx, agent) => {
1270
+ // A pre-turn prefix includes admission but excludes the later inbox claim.
1271
+ // Cancel that inherited work durably before publishing the editable fork.
1272
+ agent.inbox.clear();
1273
+ configured = await setupAgentContext(agentCtx, agent);
1274
+ },
975
1275
  });
976
1276
  try {
977
1277
  this.assertActive(agent);
@@ -987,7 +1287,6 @@ export class SessionRuntime {
987
1287
  }
988
1288
  await this.#activate({
989
1289
  handle,
990
- selection: ref,
991
1290
  contextWindow: undefined,
992
1291
  reasoningEffort: undefined,
993
1292
  agentPreset: configuration.agentPreset,
@@ -1095,34 +1394,41 @@ export class SessionRuntime {
1095
1394
  /**
1096
1395
  * Rebuild the argument vocabularies the composer completes.
1097
1396
  *
1098
- * A snapshot, because completion runs per keystroke: `/model provider/model`,
1099
- * `/resume <id>` and `/login <provider>` are all names that already exist in
1397
+ * A snapshot, because completion runs per keystroke. `/model provider/model`
1398
+ * and `/resume <id>` are names that already exist in
1100
1399
  * this process, so the reader types toward them instead of opening a picker.
1101
1400
  */
1102
1401
  async #refreshArgumentOptions() {
1103
- const options = new Map();
1402
+ const epoch = ++this.#argumentEpoch;
1104
1403
  const llm = this.#ctx.get('llm');
1105
- if (llm !== undefined) {
1106
- const models = [];
1107
- const providers = [];
1108
- for (const provider of llm.listProviders()) {
1109
- providers.push({ value: provider.id, ...(provider.name === undefined ? {} : { description: provider.name }) });
1110
- for (const model of await llm.listModels(provider.id)) {
1111
- models.push({
1112
- value: `${provider.id}/${model.id}`,
1113
- ...(model.name === undefined ? {} : { description: model.name }),
1114
- });
1115
- }
1404
+ const providers = llm?.listProviders() ?? [];
1405
+ const previous = this.#argumentOptions.get('model') ?? [];
1406
+ const groups = new Map(providers.map(provider => [provider.id, previous.filter(model => model.value.startsWith(`${provider.id}/`))]));
1407
+ const options = new Map([['resume', sessionArgumentOptions(this.#recent)]]);
1408
+ const publish = () => {
1409
+ if (this.#disposed || epoch !== this.#argumentEpoch)
1410
+ return;
1411
+ options.set('model', providers.flatMap(provider => groups.get(provider.id) ?? []));
1412
+ this.#argumentOptions = options;
1413
+ this.#pushCommands();
1414
+ };
1415
+ publish();
1416
+ await Promise.all(providers.map(async (provider) => {
1417
+ try {
1418
+ const models = await llm?.listModels(provider.id) ?? [];
1419
+ if (this.#disposed || epoch !== this.#argumentEpoch)
1420
+ return;
1421
+ groups.set(provider.id, models.map(model => ({
1422
+ value: `${provider.id}/${model.id}`,
1423
+ ...(model.name === undefined ? {} : { description: model.name }),
1424
+ })));
1116
1425
  }
1117
- options.set('model', models);
1118
- options.set('login', providers);
1119
- }
1120
- options.set('resume', this.#recent.map(row => ({
1121
- value: row.id,
1122
- ...(row.title === undefined ? {} : { description: row.title }),
1123
- })));
1124
- this.#argumentOptions = options;
1125
- this.#pushCommands();
1426
+ catch {
1427
+ // Preserve the last successful catalog. The model picker owns diagnostics.
1428
+ return;
1429
+ }
1430
+ publish();
1431
+ }));
1126
1432
  }
1127
1433
  async refreshRecent() {
1128
1434
  // Coalesce concurrent callers: the recent list is one snapshot, and a
@@ -1140,15 +1446,18 @@ export class SessionRuntime {
1140
1446
  break;
1141
1447
  this.#refreshDirty = false;
1142
1448
  }
1143
- })().finally(() => { this.#refreshInFlight = null; });
1449
+ })().finally(() => {
1450
+ this.#refreshInFlight = null;
1451
+ // Completion must read the newly published rows, not the previous
1452
+ // directory's snapshot while persistence.list() is still pending.
1453
+ queueMicrotask(() => { if (!this.#disposed)
1454
+ void this.#refreshArgumentOptions().catch(() => { }); });
1455
+ });
1144
1456
  this.#refreshInFlight = run;
1145
1457
  return run;
1146
1458
  }
1147
1459
  async #refreshRecentNow() {
1148
1460
  this.#archivedSessions = new Set(readSessionLibrary(this.#libraryPath).archived);
1149
- // The recent list is what `/resume` completes from, so a refresh of one is a
1150
- // refresh of the other. Deferred to avoid recursing through #pushCommands.
1151
- queueMicrotask(() => { void this.#refreshArgumentOptions().catch(() => { }); });
1152
1461
  const persistence = this.#ctx.get('sessionPersistence');
1153
1462
  if (persistence === undefined) {
1154
1463
  this.#recent = [];
@@ -1157,6 +1466,8 @@ export class SessionRuntime {
1157
1466
  }
1158
1467
  const snapshots = (await persistence.list()).filter(snapshot => snapshot.header.origin !== 'subagent')
1159
1468
  .sort((left, right) => right.header.createdAt - left.header.createdAt);
1469
+ const agent = this.agent;
1470
+ const cwd = agent === undefined ? process.cwd() : currentWorkingDirectory(this.#ctx, agent.session);
1160
1471
  // A stored row stays valid while its revision holds, so a cold process
1161
1472
  // renders labels for unchanged sessions without reading their logs; only a
1162
1473
  // new or changed session pays a read.
@@ -1176,6 +1487,10 @@ export class SessionRuntime {
1176
1487
  }
1177
1488
  for (const snapshot of snapshots) {
1178
1489
  const header = snapshot.header;
1490
+ // Scope before opening logs or consulting shared row caches. Other
1491
+ // directories remain available through an explicit session ID.
1492
+ if (header.cwd !== cwd)
1493
+ continue;
1179
1494
  const cached = this.#recentCache.get(header.id);
1180
1495
  if (snapshot.formatStatus === 'migration-required') {
1181
1496
  // Do not open the log merely to browse it. A previous label is a hint;
@@ -1220,10 +1535,15 @@ export class SessionRuntime {
1220
1535
  }
1221
1536
  async dispose() {
1222
1537
  this.#queueAbort?.abort(new Error('session runtime disposed'));
1538
+ this.#workflowAbort?.abort();
1539
+ for (const abort of this.#descendantApprovals.keys())
1540
+ abort.abort();
1223
1541
  if (this.#disposed)
1224
1542
  return;
1225
1543
  this.#disposed = true;
1544
+ this.#activeInteraction.abort();
1226
1545
  this.#activationEpoch += 1;
1546
+ this.#cancelInspectDeliveries();
1227
1547
  this.#inspectedId = undefined;
1228
1548
  this.#tracker.reset();
1229
1549
  this.#tui.setInspectedSubagent(undefined);
@@ -1243,15 +1563,14 @@ export class SessionRuntime {
1243
1563
  this.#releasedHandles.add(handle);
1244
1564
  await this.#disposeHandles(handles);
1245
1565
  }
1246
- async #create(selection) {
1247
- const ref = { current: selection, assembled: undefined };
1248
- const preset = await this.#ctx.agentPresets.resolve();
1566
+ async #create(selection, presetId) {
1567
+ const preset = await this.#ctx.agentPresets.resolve(presetId);
1249
1568
  let configured;
1250
1569
  const handle = await this.#ctx.agents.create({
1251
1570
  sessionId: SessionId('session-' + randomUUID()),
1252
1571
  meta: { cwd: process.cwd(), agentPreset: preset.id },
1253
1572
  agentOptions: { provider: selection.provider, model: selection.model },
1254
- setup: async (agentCtx, agent) => { configured = await setupAgentContext(agentCtx, agent, ref); },
1573
+ setup: async (agentCtx, agent) => { configured = await setupAgentContext(agentCtx, agent); },
1255
1574
  });
1256
1575
  const configuration = configured;
1257
1576
  if (configuration === undefined) {
@@ -1260,7 +1579,6 @@ export class SessionRuntime {
1260
1579
  }
1261
1580
  return {
1262
1581
  handle,
1263
- selection: ref,
1264
1582
  contextWindow: undefined,
1265
1583
  reasoningEffort: undefined,
1266
1584
  agentPreset: configuration.agentPreset,
@@ -1305,6 +1623,9 @@ export class SessionRuntime {
1305
1623
  const previous = this.#active;
1306
1624
  const epoch = ++this.#activationEpoch;
1307
1625
  this.#queueAbort?.abort(new Error('active session changed'));
1626
+ this.#workflowAbort?.abort();
1627
+ this.#activeInteraction.abort();
1628
+ this.#activeInteraction = new AbortController();
1308
1629
  this.#active = next;
1309
1630
  void this.#claimWorkspace(next);
1310
1631
  try {
@@ -1338,6 +1659,8 @@ export class SessionRuntime {
1338
1659
  // and the activation failure remains the actionable error.
1339
1660
  try {
1340
1661
  if (this.#activationEpoch === epoch) {
1662
+ this.#activeInteraction.abort();
1663
+ this.#activeInteraction = new AbortController();
1341
1664
  this.#active = previous;
1342
1665
  if (previous !== undefined) {
1343
1666
  try {
@@ -1370,8 +1693,12 @@ export class SessionRuntime {
1370
1693
  }
1371
1694
  /** Show one active session on the terminal: transcript, tools, controls, model. */
1372
1695
  #presentAgent(active, reason = 'open') {
1696
+ for (const [abort, root] of this.#descendantApprovals)
1697
+ if (root !== active.handle.agent)
1698
+ abort.abort();
1373
1699
  this.#workflows.reset(active.handle.agent.session.snapshotEvents());
1374
1700
  const agent = active.handle.agent;
1701
+ this.#cancelInspectDeliveries();
1375
1702
  this.#inspectedId = undefined;
1376
1703
  this.#tui.setInspectedSubagent(undefined);
1377
1704
  this.#tui.setStatus(agent.status);
@@ -1410,8 +1737,9 @@ export class SessionRuntime {
1410
1737
  ...(options === undefined ? {} : { complete: (prefix) => matchArgumentOptions(options, prefix) }),
1411
1738
  };
1412
1739
  });
1413
- const names = new Set(commands.map(command => command.name));
1414
- for (const skill of this.#skillCommands) {
1740
+ const names = new Set([...commands.map(command => command.name),
1741
+ ...BUILTIN_SLASH_COMMANDS.flatMap(command => [command.name, ...(command.aliases ?? [])])]);
1742
+ for (const skill of userSkillCommands(this.#skills, names)) {
1415
1743
  if (names.has(skill.name))
1416
1744
  continue;
1417
1745
  commands.push(skill);
@@ -1433,14 +1761,14 @@ export class SessionRuntime {
1433
1761
  const agent = this.agent;
1434
1762
  const skills = this.#ctx.get('skills');
1435
1763
  if (agent === undefined || skills === undefined) {
1436
- this.#skillCommands = [];
1764
+ this.#skills = [];
1437
1765
  }
1438
1766
  else {
1439
1767
  const cwd = currentWorkingDirectory(this.#ctx, agent.session);
1440
1768
  const list = await skills.list({ cwd, scope: agent, signal });
1441
1769
  if (this.#disposed || this.agent !== agent || currentWorkingDirectory(this.#ctx, agent.session) !== cwd)
1442
1770
  return;
1443
- this.#skillCommands = userSkillCommands(list);
1771
+ this.#skills = list;
1444
1772
  }
1445
1773
  this.#pushCommands();
1446
1774
  }
@@ -1487,6 +1815,7 @@ export class SessionRuntime {
1487
1815
  id,
1488
1816
  label: view?.label ?? id,
1489
1817
  phase: view?.phase ?? fallbackPhase,
1818
+ needsApproval: view?.needsApproval === true,
1490
1819
  ...(mode === undefined ? {} : { mode }),
1491
1820
  writable: isSteerableSubagent(mode),
1492
1821
  };
@@ -1512,6 +1841,7 @@ export class SessionRuntime {
1512
1841
  #replaceTranscript(agent, reason = 'refresh') {
1513
1842
  this.#tui.setWorkingDirectory?.(currentWorkingDirectory(this.#ctx, agent.session));
1514
1843
  const events = agent.session.snapshotEvents();
1844
+ this.#tui.pendingInputs?.reconcile?.(agent.id, new Set(events.flatMap(event => event.type === 'agent/inbox/spliced' ? event.data.inserted.map(message => message.id) : [])));
1515
1845
  this.#tracker.observeCatalog(agent.session, events);
1516
1846
  this.#tui.replaceSession(events, this.#ctx.get('tuiToolPresentation')?.session(agent, events), agent.status, reason);
1517
1847
  this.#replayLivePrefix(agent.session.id);
@@ -1525,10 +1855,18 @@ export class SessionRuntime {
1525
1855
  if (agent !== undefined)
1526
1856
  this.#replaceTranscript(agent);
1527
1857
  }
1858
+ #cancelInspectDeliveries() {
1859
+ for (const delivery of this.#inspectDeliveries)
1860
+ delivery.abort();
1861
+ this.#inspectDeliveries.clear();
1862
+ }
1528
1863
  #closeInspect() {
1529
1864
  if (this.#inspectedId === undefined)
1530
1865
  return;
1531
1866
  this.#inspectEpoch += 1;
1867
+ this.#queueAbort?.abort(new Error('viewed session changed'));
1868
+ this.#workflowAbort?.abort();
1869
+ this.#cancelInspectDeliveries();
1532
1870
  this.#inspectedId = undefined;
1533
1871
  this.#tui.setInspectedSubagent(undefined);
1534
1872
  const agent = this.#active?.handle.agent;
@@ -1573,6 +1911,12 @@ export class SessionRuntime {
1573
1911
  }
1574
1912
  if (request !== this.#inspectEpoch)
1575
1913
  return;
1914
+ if (this.#inspectedId !== id) {
1915
+ this.#queueAbort?.abort(new Error('viewed session changed'));
1916
+ this.#workflowAbort?.abort();
1917
+ this.#cancelInspectDeliveries();
1918
+ }
1919
+ this.#tui.pendingInputs?.reconcile?.(id, new Set(events.flatMap(event => event.type === 'agent/inbox/spliced' ? event.data.inserted.map(message => message.id) : [])));
1576
1920
  this.#inspectedId = id;
1577
1921
  if (cwd !== undefined)
1578
1922
  this.#tui.setWorkingDirectory?.(cwd);
@@ -1586,12 +1930,12 @@ export class SessionRuntime {
1586
1930
  const childId = this.#inspectedId;
1587
1931
  const root = this.#active?.handle.agent;
1588
1932
  if (childId === undefined || root === undefined) {
1589
- this.#tui.restoreInput(submission);
1933
+ this.#tui.restoreInput(submission, childId);
1590
1934
  return;
1591
1935
  }
1592
1936
  const view = this.#inspectView(childId);
1593
1937
  if (!view.writable) {
1594
- this.#tui.restoreInput(submission);
1938
+ this.#tui.restoreInput(submission, childId);
1595
1939
  this.#tui.notice('This subagent is a completed run and cannot take more messages.');
1596
1940
  return;
1597
1941
  }
@@ -1603,34 +1947,60 @@ export class SessionRuntime {
1603
1947
  : this.#ctx.get('agents')?.get(SessionId(parentId));
1604
1948
  const subagents = this.#ctx.get('subagents');
1605
1949
  if (parent === undefined || subagents === undefined) {
1606
- this.#tui.restoreInput(submission);
1950
+ this.#tui.restoreInput(submission, childId);
1607
1951
  this.#tui.notice(parent === undefined
1608
1952
  ? 'The parent of this subagent is not live, so it cannot take a follow-up.'
1609
1953
  : 'Subagent follow-up is unavailable in this composition.');
1610
1954
  return;
1611
1955
  }
1956
+ const previous = this.#inspectDeliveryTail;
1957
+ let release = () => { };
1958
+ this.#inspectDeliveryTail = new Promise(resolve => { release = resolve; });
1959
+ const delivery = new AbortController();
1960
+ this.#inspectDeliveries.add(delivery);
1961
+ let cancelUpload = () => { };
1962
+ const cancelled = new Promise((_resolve, reject) => {
1963
+ cancelUpload = () => { reject(delivery.signal.reason); };
1964
+ delivery.signal.addEventListener('abort', cancelUpload, { once: true });
1965
+ });
1612
1966
  try {
1613
- const message = await createSubmissionMessage(submission, this.#ctx.get('attachments'));
1614
- if (this.#inspectedId !== childId || this.#active?.handle.agent !== root) {
1615
- this.#tui.restoreInput(submission);
1616
- return;
1617
- }
1618
- await queueHostSubagentPrompt(subagents, parent, SessionId(childId), message.content, { kind: 'user' }, new AbortController().signal);
1967
+ // Recalled messages can include attachments; admission must retain submission order.
1968
+ await Promise.race([previous, cancelled]);
1969
+ delivery.signal.throwIfAborted();
1970
+ // Attachment storage need not support cancellation; release the draft now
1971
+ // and ignore a later upload result before crossing Harness admission.
1972
+ const message = await Promise.race([createSubmissionMessage(submission, this.#ctx.get('attachments')), cancelled]);
1973
+ delivery.signal.removeEventListener('abort', cancelUpload);
1974
+ delivery.signal.throwIfAborted();
1975
+ if (this.#inspectedId !== childId || this.#active?.handle.agent !== root)
1976
+ throw new Error('The viewed session changed before delivery.');
1977
+ const send = submission.delivery === 'steer' ? steerHostSubagentPrompt : queueHostSubagentPrompt;
1978
+ const messageId = await send(subagents, parent, SessionId(childId), message.content, { kind: 'user' }, delivery.signal);
1979
+ this.#tui.pendingInputs?.prepare?.(submission, messageId);
1980
+ this.#tui.pendingInputs?.acknowledge?.(submission);
1619
1981
  }
1620
1982
  catch (error) {
1621
- this.#tui.restoreInput(submission);
1622
- this.#tui.notice(error instanceof Error ? error.message : String(error), { level: 'error' });
1983
+ this.#tui.restoreInput(submission, childId);
1984
+ if (!delivery.signal.aborted)
1985
+ this.#tui.notice(error instanceof Error ? error.message : String(error), { level: 'error' });
1986
+ }
1987
+ finally {
1988
+ delivery.signal.removeEventListener('abort', cancelUpload);
1989
+ this.#inspectDeliveries.delete(delivery);
1990
+ release();
1623
1991
  }
1624
1992
  }
1625
1993
  #sessionControls(active, projection) {
1626
- const controls = sessionControls(projection);
1627
- const livePlan = this.#ctx.get('planMode')?.get(active.handle.agent);
1994
+ const liveGoal = this.#ctx.get('goals')?.get(active.handle.agent);
1995
+ const controls = sessionControls(projection, this.#ctx.get('planMode')?.get(active.handle.agent));
1996
+ if (controls.goal !== undefined && liveGoal !== undefined && liveGoal.id === projection?.goal?.goal.id && liveGoal.revision === projection?.goal?.goal.revision) {
1997
+ controls.goal.activation = liveGoal.activation;
1998
+ }
1999
+ const failure = active.handle.agent.status === 'idle' ? continuableFailure(active.handle.agent.session.snapshotEvents()) : undefined;
1628
2000
  return {
1629
2001
  ...controls,
2002
+ ...(failure === undefined ? {} : { continuation: failure }),
1630
2003
  agentPreset: active.agentPreset,
1631
- ...(livePlan === undefined
1632
- ? {}
1633
- : { plan: { active: livePlan.active, pending: livePlan.pending !== undefined } }),
1634
2004
  };
1635
2005
  }
1636
2006
  /** Read one client-visible snapshot. Host-only projection state is never consulted. */
@@ -1672,6 +2042,7 @@ export function matchArgumentOptions(options, prefix) {
1672
2042
  if (lower === '')
1673
2043
  return options;
1674
2044
  return options.filter(option => option.value.toLowerCase().includes(lower)
2045
+ || (option.label ?? '').toLowerCase().includes(lower)
1675
2046
  || (option.description ?? '').toLowerCase().includes(lower));
1676
2047
  }
1677
2048
  //# sourceMappingURL=session-controller.js.map