@gajae-code/coding-agent 0.11.10 → 0.12.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.
Files changed (249) hide show
  1. package/CHANGELOG.md +86 -2
  2. package/dist/types/cli/read-cli.d.ts +1 -0
  3. package/dist/types/cli.d.ts +8 -1
  4. package/dist/types/commands/deep-interview.d.ts +9 -0
  5. package/dist/types/commands/harness.d.ts +22 -0
  6. package/dist/types/commands/read.d.ts +6 -0
  7. package/dist/types/config/model-profile-activation.d.ts +1 -0
  8. package/dist/types/config/model-profile-contract.d.ts +2 -0
  9. package/dist/types/config/model-registry.d.ts +4 -0
  10. package/dist/types/config/provider-auth-health.d.ts +14 -0
  11. package/dist/types/config/provider-ranking.d.ts +59 -0
  12. package/dist/types/config/settings-schema.d.ts +86 -3
  13. package/dist/types/config/settings.d.ts +3 -0
  14. package/dist/types/extensibility/extensions/types.d.ts +43 -2
  15. package/dist/types/extensibility/shared-events.d.ts +6 -0
  16. package/dist/types/gjc-runtime/deep-interview-ambiguity.d.ts +5 -12
  17. package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +17 -32
  18. package/dist/types/gjc-runtime/deep-interview-stage.d.ts +37 -0
  19. package/dist/types/gjc-runtime/deep-interview-state.d.ts +4 -135
  20. package/dist/types/gjc-runtime/gc-runtime.d.ts +1 -1
  21. package/dist/types/gjc-runtime/linux-proc.d.ts +22 -14
  22. package/dist/types/gjc-runtime/memory-guard-owner-claims.d.ts +47 -0
  23. package/dist/types/gjc-runtime/ralplan-runtime.d.ts +112 -1
  24. package/dist/types/gjc-runtime/state-runtime.d.ts +3 -1
  25. package/dist/types/gjc-runtime/state-writer.d.ts +5 -46
  26. package/dist/types/gjc-runtime/team-launch.d.ts +1 -1
  27. package/dist/types/gjc-runtime/team-runtime.d.ts +2 -1
  28. package/dist/types/gjc-runtime/team-store.d.ts +12 -0
  29. package/dist/types/gjc-runtime/team-worker-memory-guard.d.ts +92 -0
  30. package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +7 -0
  31. package/dist/types/gjc-runtime/ultragoal-receipt-freshness.d.ts +8 -0
  32. package/dist/types/gjc-runtime/workflow-manifest.d.ts +3 -5
  33. package/dist/types/internal-urls/local-root-gc.d.ts +2 -0
  34. package/dist/types/main.d.ts +1 -1
  35. package/dist/types/memories/index.d.ts +2 -0
  36. package/dist/types/modes/acp/acp-agent.d.ts +22 -9
  37. package/dist/types/modes/components/assistant-message.d.ts +3 -1
  38. package/dist/types/modes/components/irc-sidebar.d.ts +41 -0
  39. package/dist/types/modes/components/queue-pane.d.ts +8 -0
  40. package/dist/types/modes/components/queued-message-selector.d.ts +6 -0
  41. package/dist/types/modes/components/read-tool-group.d.ts +4 -4
  42. package/dist/types/modes/components/session-observer-overlay.d.ts +4 -0
  43. package/dist/types/modes/components/settings-selector.d.ts +2 -0
  44. package/dist/types/modes/components/tool-execution.d.ts +8 -0
  45. package/dist/types/modes/components/welcome.d.ts +1 -0
  46. package/dist/types/modes/controllers/event-controller.d.ts +3 -0
  47. package/dist/types/modes/interactive-mode.d.ts +7 -1
  48. package/dist/types/modes/irc-observation-ledger.d.ts +17 -1
  49. package/dist/types/modes/types.d.ts +2 -0
  50. package/dist/types/modes/utils/ui-helpers.d.ts +16 -0
  51. package/dist/types/runtime/memory-domain.d.ts +9 -0
  52. package/dist/types/runtime/memory-guard-contract.d.ts +71 -0
  53. package/dist/types/runtime/memory-guard.d.ts +39 -0
  54. package/dist/types/runtime/memory-limit.d.ts +11 -0
  55. package/dist/types/runtime-mcp/manager.d.ts +8 -0
  56. package/dist/types/runtime-mcp/smithery-auth.d.ts +5 -0
  57. package/dist/types/sdk/acp/final-text.d.ts +19 -0
  58. package/dist/types/sdk/acp/mcp.d.ts +23 -0
  59. package/dist/types/sdk/broker/lifecycle.d.ts +7 -0
  60. package/dist/types/sdk/bus/chat-daemon-control.d.ts +2 -1
  61. package/dist/types/sdk/bus/control-drain-lease.d.ts +52 -0
  62. package/dist/types/sdk/bus/index.d.ts +18 -2
  63. package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +37 -0
  64. package/dist/types/sdk/bus/prompt-reconciliation.d.ts +2 -1
  65. package/dist/types/sdk/bus/rate-limit-pool.d.ts +2 -0
  66. package/dist/types/sdk/bus/reconciliation-store.d.ts +65 -0
  67. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +6 -4
  68. package/dist/types/sdk/bus/telegram-daemon.d.ts +23 -3
  69. package/dist/types/sdk/host/control/operations.d.ts +2 -1
  70. package/dist/types/sdk/host/host.d.ts +2 -0
  71. package/dist/types/sdk/host/query/handlers.d.ts +5 -0
  72. package/dist/types/sdk/lifecycle-session.d.ts +10 -1
  73. package/dist/types/sdk/startup-capability.d.ts +6 -0
  74. package/dist/types/session/agent-session.d.ts +51 -19
  75. package/dist/types/session/fallback-chain-controller.d.ts +18 -2
  76. package/dist/types/session/internal/managed-session-scope.d.ts +27 -0
  77. package/dist/types/session/memory-guard-checkpoint-participant.d.ts +69 -0
  78. package/dist/types/session/session-manager.d.ts +59 -13
  79. package/dist/types/session/streaming-output.d.ts +60 -5
  80. package/dist/types/skill-state/active-state.d.ts +0 -4
  81. package/dist/types/skill-state/workflow-hud.d.ts +3 -0
  82. package/dist/types/task/executor.d.ts +6 -0
  83. package/dist/types/task/types.d.ts +36 -0
  84. package/dist/types/task/ultragoal-redteam-activation.d.ts +33 -0
  85. package/dist/types/tools/browser/launch.d.ts +7 -0
  86. package/dist/types/tools/fetch.d.ts +17 -1
  87. package/dist/types/tools/output-meta.d.ts +21 -1
  88. package/dist/types/tools/read-internals.d.ts +12 -0
  89. package/dist/types/tools/read.d.ts +45 -1
  90. package/dist/types/tools/resource-gc.d.ts +23 -0
  91. package/dist/types/tools/tool-result.d.ts +7 -1
  92. package/package.json +9 -13
  93. package/scripts/benchmark-sticky-viewport-pr1.ts +239 -0
  94. package/scripts/capture-sticky-viewport-showcase.ts +292 -0
  95. package/scripts/generate-sdk-operation-inventory.ts +4 -0
  96. package/scripts/run-test-manifest.ts +6 -4
  97. package/scripts/verify-gjc-sdk-canonicalization.ts +122 -18
  98. package/scripts/verify-sticky-viewport-showcase.ts +611 -0
  99. package/src/capability/index.ts +8 -0
  100. package/src/cli/args.ts +14 -8
  101. package/src/cli/fast-help.ts +1 -1
  102. package/src/cli/read-cli.ts +2 -1
  103. package/src/cli.ts +49 -2
  104. package/src/commands/deep-interview.ts +29 -9
  105. package/src/commands/harness.ts +35 -12
  106. package/src/commands/read.ts +11 -2
  107. package/src/commands/sdk.ts +91 -7
  108. package/src/commands/team.ts +1 -1
  109. package/src/config/model-profile-activation.ts +1 -0
  110. package/src/config/model-profile-contract.ts +13 -0
  111. package/src/config/model-profiles.ts +24 -0
  112. package/src/config/model-registry.ts +21 -2
  113. package/src/config/provider-auth-health.ts +42 -0
  114. package/src/config/provider-ranking.ts +119 -0
  115. package/src/config/settings-schema.ts +84 -3
  116. package/src/config/settings.ts +254 -108
  117. package/src/defaults/gjc/skills/deep-interview/SKILL.md +229 -82
  118. package/src/defaults/gjc/skills/ralplan/SKILL.md +78 -26
  119. package/src/defaults/gjc/skills/team/SKILL.md +1 -1
  120. package/src/defaults/gjc/skills/ultragoal/SKILL.md +26 -16
  121. package/src/eval/py/executor.ts +7 -2
  122. package/src/extensibility/extensions/runner.ts +4 -0
  123. package/src/extensibility/extensions/types.ts +42 -4
  124. package/src/extensibility/shared-events.ts +5 -0
  125. package/src/gjc-runtime/deep-interview-ambiguity.ts +47 -68
  126. package/src/gjc-runtime/deep-interview-recorder.ts +227 -310
  127. package/src/gjc-runtime/deep-interview-runtime.ts +109 -270
  128. package/src/gjc-runtime/deep-interview-stage.ts +1016 -0
  129. package/src/gjc-runtime/deep-interview-state.ts +12 -921
  130. package/src/gjc-runtime/gc-render.ts +1 -0
  131. package/src/gjc-runtime/gc-runtime.ts +11 -1
  132. package/src/gjc-runtime/linux-proc.ts +70 -36
  133. package/src/gjc-runtime/memory-guard-owner-claims.ts +389 -0
  134. package/src/gjc-runtime/ralplan-runtime.ts +979 -71
  135. package/src/gjc-runtime/state-runtime.ts +50 -12
  136. package/src/gjc-runtime/state-writer.ts +70 -222
  137. package/src/gjc-runtime/team-launch.ts +34 -5
  138. package/src/gjc-runtime/team-runtime.ts +970 -33
  139. package/src/gjc-runtime/team-store.ts +32 -0
  140. package/src/gjc-runtime/team-worker-memory-guard.ts +363 -0
  141. package/src/gjc-runtime/team-workers.ts +1 -0
  142. package/src/gjc-runtime/tmux-owner-isolation.ts +18 -0
  143. package/src/gjc-runtime/ultragoal-receipt-freshness.ts +20 -0
  144. package/src/gjc-runtime/ultragoal-runtime.ts +13 -7
  145. package/src/gjc-runtime/workflow-manifest.generated.json +97 -350
  146. package/src/gjc-runtime/workflow-manifest.ts +34 -179
  147. package/src/hooks/native-skill-hook.ts +17 -4
  148. package/src/hooks/skill-state.ts +1 -23
  149. package/src/internal-urls/docs-index.generated.ts +10 -11
  150. package/src/internal-urls/local-protocol.ts +23 -4
  151. package/src/internal-urls/local-root-gc.ts +127 -0
  152. package/src/main.ts +9 -5
  153. package/src/memories/index.ts +10 -0
  154. package/src/modes/DESIGN.md +47 -0
  155. package/src/modes/acp/acp-agent.ts +597 -67
  156. package/src/modes/acp/acp-event-mapper.ts +106 -12
  157. package/src/modes/acp/acp-mode.ts +39 -3
  158. package/src/modes/components/assistant-message.ts +123 -22
  159. package/src/modes/components/irc-sidebar.ts +176 -62
  160. package/src/modes/components/model-selector.ts +76 -5
  161. package/src/modes/components/notifications-settings-editor.ts +1 -1
  162. package/src/modes/components/oauth-selector.ts +42 -12
  163. package/src/modes/components/queue-pane.ts +55 -9
  164. package/src/modes/components/queued-message-selector.ts +53 -10
  165. package/src/modes/components/read-tool-group.ts +82 -14
  166. package/src/modes/components/session-observer-overlay.ts +187 -32
  167. package/src/modes/components/settings-selector.ts +60 -18
  168. package/src/modes/components/tool-execution.ts +138 -27
  169. package/src/modes/components/tool-status-header.ts +1 -4
  170. package/src/modes/components/welcome.ts +18 -3
  171. package/src/modes/controllers/command-controller.ts +4 -0
  172. package/src/modes/controllers/event-controller.ts +109 -27
  173. package/src/modes/controllers/extension-ui-controller.ts +23 -12
  174. package/src/modes/controllers/input-controller.ts +62 -6
  175. package/src/modes/controllers/selector-controller.ts +83 -37
  176. package/src/modes/interactive-mode.ts +127 -29
  177. package/src/modes/irc-observation-ledger.ts +71 -15
  178. package/src/modes/runtime-init.ts +20 -2
  179. package/src/modes/types.ts +2 -0
  180. package/src/modes/utils/ui-helpers.ts +94 -1
  181. package/src/prompts/agents/architect.md +9 -1
  182. package/src/prompts/agents/critic.md +9 -0
  183. package/src/prompts/agents/executor.md +1 -1
  184. package/src/prompts/system/system-prompt.md +0 -1
  185. package/src/prompts/tools/read.md +4 -2
  186. package/src/runtime/memory-domain.ts +66 -0
  187. package/src/runtime/memory-guard-contract.ts +73 -0
  188. package/src/runtime/memory-guard.ts +292 -0
  189. package/src/runtime/memory-limit.ts +50 -0
  190. package/src/runtime-mcp/manager.ts +26 -6
  191. package/src/runtime-mcp/smithery-auth.ts +17 -2
  192. package/src/runtime-mcp/smithery-connect.ts +11 -1
  193. package/src/sdk/acp/adapter.ts +8 -1
  194. package/src/sdk/acp/final-text.ts +40 -0
  195. package/src/sdk/acp/mcp.ts +26 -0
  196. package/src/sdk/broker/lifecycle.ts +107 -7
  197. package/src/sdk/bus/chat-daemon-cli.ts +2 -2
  198. package/src/sdk/bus/chat-daemon-control.ts +4 -3
  199. package/src/sdk/bus/config.ts +1 -1
  200. package/src/sdk/bus/control-drain-lease.ts +104 -0
  201. package/src/sdk/bus/index.ts +503 -60
  202. package/src/sdk/bus/kind-aware-reconciliation.ts +242 -0
  203. package/src/sdk/bus/prompt-reconciliation.ts +2 -1
  204. package/src/sdk/bus/rate-limit-pool.ts +8 -0
  205. package/src/sdk/bus/reconciliation-store.ts +224 -0
  206. package/src/sdk/bus/telegram-daemon-contract.ts +6 -4
  207. package/src/sdk/bus/telegram-daemon.ts +531 -53
  208. package/src/sdk/host/control/dispatch.ts +7 -1
  209. package/src/sdk/host/control/operations.ts +2 -1
  210. package/src/sdk/host/host.ts +25 -3
  211. package/src/sdk/host/query/handlers.ts +55 -0
  212. package/src/sdk/lifecycle-session.ts +19 -3
  213. package/src/sdk/mcp/server.ts +46 -18
  214. package/src/sdk/protocol/operation-inventory.generated.json +76 -0
  215. package/src/sdk/protocol/operation-registry.ts +8 -2
  216. package/src/sdk/session.ts +25 -2
  217. package/src/sdk/startup-capability.ts +6 -0
  218. package/src/session/agent-session.ts +720 -190
  219. package/src/session/fallback-chain-controller.ts +47 -8
  220. package/src/session/internal/managed-session-scope.ts +40 -10
  221. package/src/session/internal/native-publish-outcome.ts +8 -3
  222. package/src/session/memory-guard-checkpoint-participant.ts +131 -0
  223. package/src/session/session-manager.ts +1372 -42
  224. package/src/session/streaming-output.ts +262 -43
  225. package/src/setup/provider-onboarding.ts +19 -4
  226. package/src/skill-state/active-state.ts +0 -10
  227. package/src/skill-state/workflow-hud.ts +25 -2
  228. package/src/skill-state/workflow-mutation-guard.ts +65 -7
  229. package/src/task/executor.ts +11 -3
  230. package/src/task/index.ts +2 -0
  231. package/src/task/types.ts +22 -11
  232. package/src/task/ultragoal-redteam-activation.ts +75 -0
  233. package/src/tools/ask.ts +89 -69
  234. package/src/tools/browser/launch.ts +41 -6
  235. package/src/tools/fetch.ts +114 -23
  236. package/src/tools/output-meta.ts +176 -9
  237. package/src/tools/path-utils.ts +5 -1
  238. package/src/tools/read-internals.ts +44 -0
  239. package/src/tools/read.ts +1542 -310
  240. package/src/tools/resource-gc.ts +665 -125
  241. package/src/tools/tool-result.ts +14 -1
  242. package/src/web/search/providers/codex.ts +1 -1
  243. package/src/workflow/workflow-intent-diff.ts +4 -1
  244. package/dist/types/gjc-runtime/deep-interview-draft.d.ts +0 -8
  245. package/dist/types/gjc-runtime/deep-interview-payload.d.ts +0 -41
  246. package/dist/types/gjc-runtime/deep-interview-repair.d.ts +0 -17
  247. package/src/gjc-runtime/deep-interview-draft.ts +0 -843
  248. package/src/gjc-runtime/deep-interview-payload.ts +0 -262
  249. package/src/gjc-runtime/deep-interview-repair.ts +0 -1674
@@ -30,6 +30,7 @@ import {
30
30
  getPluginsCacheDir,
31
31
  MarketplaceManager,
32
32
  } from "../../extensibility/plugins/marketplace";
33
+ import { INTERACTIVE_SELECTOR_RESUME_ORIGIN } from "../../extensibility/shared-events";
33
34
  import {
34
35
  getAvailableThemes,
35
36
  getCurrentThemeName,
@@ -165,6 +166,7 @@ import type { JobsObserver } from "../jobs-observer";
165
166
  import type { SessionObserverRegistry } from "../session-observer-registry";
166
167
  import type { TasksAggregator } from "../tasks-aggregator";
167
168
  import type { TranscriptItemRegistry } from "../transcript-item-registry";
169
+ import { acquireResumeProgressLease } from "../utils/ui-helpers";
168
170
 
169
171
  const CALLBACK_SERVER_PROVIDERS = new Set<string>([
170
172
  "anthropic",
@@ -1151,6 +1153,12 @@ export class SelectorController {
1151
1153
  );
1152
1154
  if (scope === undefined) return;
1153
1155
  const persistDefault = scope.trim().toLowerCase() === "default";
1156
+ if (persistDefault && !this.ctx.settings.canWriteDurableConfig()) {
1157
+ this.ctx.showError(
1158
+ "Cannot change settings while config.yml has invalid YAML syntax. Repair config.yml and reload settings.",
1159
+ );
1160
+ return;
1161
+ }
1154
1162
 
1155
1163
  const imageProvider = normalized as
1156
1164
  | "auto"
@@ -1224,13 +1232,18 @@ export class SelectorController {
1224
1232
  const selector = new ThinkingSelectorComponent(
1225
1233
  this.ctx.session.thinkingLevel,
1226
1234
  availableLevels,
1227
- selection => {
1228
- done();
1229
-
1235
+ async selection => {
1230
1236
  const { level, persistDefault } = selection;
1231
1237
  const configuredDefault = this.ctx.settings.get("defaultThinkingLevel");
1232
1238
  const levelToApply = level === ThinkingLevel.Inherit ? configuredDefault : level;
1233
- this.ctx.session.setThinkingLevel(levelToApply, persistDefault);
1239
+ try {
1240
+ await this.ctx.session.setThinkingLevelForControl(level, persistDefault);
1241
+ } catch (error) {
1242
+ this.ctx.showError(error instanceof Error ? error.message : String(error));
1243
+ return;
1244
+ }
1245
+ done();
1246
+
1234
1247
  const effectiveLevel = this.ctx.session.thinkingLevel ?? ThinkingLevel.Off;
1235
1248
  const requestedLabel =
1236
1249
  level === ThinkingLevel.Inherit ? `${level} (configured default: ${configuredDefault})` : level;
@@ -1270,6 +1283,7 @@ export class SelectorController {
1270
1283
  },
1271
1284
  {
1272
1285
  onChange: (id, value) => this.handleSettingChange(id, value),
1286
+ onError: message => this.ctx.showError(message),
1273
1287
  onThemePreview: themeName => {
1274
1288
  return previewTheme(themeName).then(result => {
1275
1289
  if (!result.success && result.error && !isThemePreviewSuperseded(result)) {
@@ -1289,6 +1303,7 @@ export class SelectorController {
1289
1303
  onPetPreview: mode => {
1290
1304
  this.ctx.previewPetMode(mode as PetMode);
1291
1305
  },
1306
+ onPetCommit: mode => this.ctx.commitPetPreviewMode(mode as PetMode),
1292
1307
  onStatusLinePreview: previewSettings => {
1293
1308
  // Update status line with preview settings
1294
1309
  this.ctx.statusLine.updateSettings({
@@ -1332,24 +1347,40 @@ export class SelectorController {
1332
1347
  getAvailableThemes().then(availableThemes => {
1333
1348
  const initialTheme = getCurrentThemeName() ?? "red-claw";
1334
1349
  this.showSelector(done => {
1350
+ const restoreAndClose = () => {
1351
+ void restoreThemePreview(initialTheme).then(result => {
1352
+ if (!result.success && result.error) {
1353
+ this.ctx.showError(`Failed to restore theme preview: ${result.error}`);
1354
+ }
1355
+ this.#refreshThemeUi();
1356
+ });
1357
+ done();
1358
+ };
1335
1359
  const selector = new ThemeSelectorComponent(
1336
1360
  initialTheme,
1337
1361
  availableThemes,
1338
1362
  themeName => {
1339
- const settingPath = getDetectedThemeSettingsPath();
1340
- settings.set(settingPath, themeName);
1341
- this.#refreshThemeUi();
1342
- done();
1343
- },
1344
- () => {
1345
- void restoreThemePreview(initialTheme).then(result => {
1346
- if (!result.success && result.error) {
1347
- this.ctx.showError(`Failed to restore theme preview: ${result.error}`);
1363
+ if (!settings.canWriteDurableConfig()) {
1364
+ this.ctx.showError(
1365
+ "Cannot change settings while config.yml has invalid YAML syntax. Repair config.yml and reload settings.",
1366
+ );
1367
+ restoreAndClose();
1368
+ return;
1369
+ }
1370
+ try {
1371
+ settings.set(getDetectedThemeSettingsPath(), themeName);
1372
+ } catch (error) {
1373
+ if (!settings.canWriteDurableConfig()) {
1374
+ this.ctx.showError(error instanceof Error ? error.message : String(error));
1375
+ restoreAndClose();
1376
+ return;
1348
1377
  }
1349
- this.#refreshThemeUi();
1350
- });
1378
+ throw error;
1379
+ }
1380
+ this.#refreshThemeUi();
1351
1381
  done();
1352
1382
  },
1383
+ restoreAndClose,
1353
1384
  themeName => {
1354
1385
  void previewTheme(themeName).then(result => {
1355
1386
  if (!result.success && result.error) {
@@ -1373,8 +1404,9 @@ export class SelectorController {
1373
1404
  const selector = new PetSelectorComponent(
1374
1405
  initial,
1375
1406
  mode => {
1376
- this.ctx.setPetMode(mode);
1377
- done();
1407
+ if (this.ctx.setPetMode(mode)) {
1408
+ done();
1409
+ }
1378
1410
  },
1379
1411
  () => {
1380
1412
  this.ctx.previewPetMode(initial);
@@ -2262,28 +2294,42 @@ export class SelectorController {
2262
2294
  async handleResumeSession(sessionPath: string): Promise<void> {
2263
2295
  const previousSessionId = this.ctx.sessionManager.getSessionId();
2264
2296
  this.#clearTransientSessionUi();
2265
- const migrationPolicy =
2266
- this.ctx.settings?.get("session.directoryMigration") === "disabled" ? "disabled" : "copy-retain";
2267
- let writableSessionPath = sessionPath;
2268
- if (this.ctx.sessionManager.isManagedDestination()) {
2269
- const inspection = await SessionManager.inspectSessionTailReadOnly(sessionPath);
2270
- if (inspection.kind === "error") throw new Error(`Could not inspect selected session: ${inspection.reason}`);
2271
- writableSessionPath = await this.ctx.sessionManager.prepareManagedCandidateForStrictAdoption(
2272
- sessionPath,
2273
- migrationPolicy,
2274
- inspection.identity,
2297
+ const progressLease = acquireResumeProgressLease(this.ctx);
2298
+ try {
2299
+ await progressLease.committed;
2300
+ const migrationPolicy =
2301
+ this.ctx.settings?.get("session.directoryMigration") === "disabled" ? "disabled" : "copy-retain";
2302
+ let writableSessionPath = sessionPath;
2303
+ if (this.ctx.sessionManager.isManagedDestination()) {
2304
+ const inspection = await SessionManager.inspectSessionTailReadOnly(sessionPath);
2305
+ if (inspection.kind === "error")
2306
+ throw new Error(`Could not inspect selected session: ${inspection.reason}`);
2307
+ writableSessionPath = await this.ctx.sessionManager.prepareManagedCandidateForStrictAdoption(
2308
+ sessionPath,
2309
+ migrationPolicy,
2310
+ inspection.identity,
2311
+ );
2312
+ }
2313
+ // Switch session via AgentSession (emits hook and tool session events)
2314
+ if (
2315
+ !(await this.ctx.session.switchSession(writableSessionPath, {
2316
+ transition: { origin: INTERACTIVE_SELECTOR_RESUME_ORIGIN },
2317
+ }))
2318
+ )
2319
+ return;
2320
+ const switchingToDifferentSession = previousSessionId !== this.ctx.sessionManager.getSessionId();
2321
+ if (switchingToDifferentSession) this.ctx.resetIrcSidebarSession();
2322
+ this.#refreshSessionTerminalTitle();
2323
+ this.ctx.updateEditorBorderColor();
2324
+
2325
+ this.ctx.rebuildInitialMessages(
2326
+ switchingToDifferentSession ? "replace-identity" : "reconcile-same-transcript",
2275
2327
  );
2328
+ await this.ctx.reloadTodos();
2329
+ this.ctx.showStatus("Resumed session");
2330
+ } finally {
2331
+ progressLease.clear();
2276
2332
  }
2277
- // Switch session via AgentSession (emits hook and tool session events)
2278
- if (!(await this.ctx.session.switchSession(writableSessionPath))) return;
2279
- const switchingToDifferentSession = previousSessionId !== this.ctx.sessionManager.getSessionId();
2280
- if (switchingToDifferentSession) this.ctx.resetIrcSidebarSession();
2281
- this.#refreshSessionTerminalTitle();
2282
- this.ctx.updateEditorBorderColor();
2283
-
2284
- this.ctx.rebuildInitialMessages(switchingToDifferentSession ? "replace-identity" : "reconcile-same-transcript");
2285
- await this.ctx.reloadTodos();
2286
- this.ctx.showStatus("Resumed session");
2287
2333
  }
2288
2334
 
2289
2335
  async handleSessionDeleteCommand(): Promise<void> {
@@ -18,6 +18,7 @@ import chalk from "chalk";
18
18
  import { AsyncJobManager } from "../async";
19
19
  import {
20
20
  type AppKeybinding,
21
+ defaultMessageQueueKeysForPlatform,
21
22
  formatKeyHint,
22
23
  formatKeyHints,
23
24
  KeybindingsManager,
@@ -47,6 +48,7 @@ import type { SessionContext, SessionManager } from "../session/session-manager"
47
48
  import { getRecentSessions, getSessionMessageEntryId } from "../session/session-manager";
48
49
  import type { LspStartupServerInfo } from "../tools";
49
50
  import { formatPhaseDisplayName } from "../tools/todo-write";
51
+ import { copyToClipboard } from "../utils/clipboard";
50
52
  import type { EventBus } from "../utils/event-bus";
51
53
  import { getSessionAccentAnsi, getSessionAccentHex } from "../utils/session-color";
52
54
  import { popTerminalTitle, pushTerminalTitle, setSessionTerminalTitle } from "../utils/title-generator";
@@ -59,7 +61,7 @@ import { GajaePetWidget, type PetMode } from "./components/gajae-pet-widget";
59
61
  import type { HookEditorComponent } from "./components/hook-editor";
60
62
  import type { HookInputComponent } from "./components/hook-input";
61
63
  import type { HookSelectorComponent } from "./components/hook-selector";
62
- import { IrcSplitViewComponent } from "./components/irc-sidebar";
64
+ import { computeIrcSplitWidths, getIrcSidebarSemanticToken, IrcSplitViewComponent } from "./components/irc-sidebar";
63
65
  import {
64
66
  getPetUnavailableWarning,
65
67
  isPetAvailable,
@@ -113,16 +115,60 @@ import {
113
115
  import type { ParsedIrcMessage } from "./utils/irc-message";
114
116
  import { addChatChild, prepareTranscriptRebuild, UiHelpers } from "./utils/ui-helpers";
115
117
 
116
- export function getDefaultComposerPlaceholder(context: KeyDisplayContext = { platform: process.platform }): string {
118
+ function buildComposerPlaceholder(
119
+ keybindings: Pick<KeybindingsManager, "getDisplayString">,
120
+ context: KeyDisplayContext,
121
+ options: { readonly busy: boolean; readonly busyPromptMode: "steer" | "queue" },
122
+ ): string {
123
+ const parts: string[] = [];
124
+ const submitKey = options.busy ? keybindings.getDisplayString("tui.input.submit", context) : "";
125
+ if (submitKey) {
126
+ const submitAction = options.busyPromptMode === "steer" ? "Steer" : "Queue";
127
+ parts.push(`${submitKey}: ${submitAction}`);
128
+ }
129
+
130
+ const queueKey = keybindings.getDisplayString("app.message.queue", context);
131
+ const submitQueues = options.busy && options.busyPromptMode === "queue" && submitKey;
132
+ if (queueKey && !submitQueues) parts.push(`${queueKey}: ${options.busy ? "Queue" : "Queue (busy)"}`);
133
+
134
+ const actionHints = [
135
+ ["app.thinking.cycle", "Thinking"],
136
+ ["app.model.select", "Model"],
137
+ ["app.history.search", "History"],
138
+ ] as const;
139
+ for (const [id, label] of actionHints) {
140
+ const key = keybindings.getDisplayString(id, context);
141
+ if (key) parts.push(`${key}: ${label}`);
142
+ }
143
+
117
144
  const newlineKeys = context.platform === "win32" ? ["alt+enter", "ctrl+j"] : ["shift+enter", "ctrl+j"];
118
- return `Type your message... ${formatKeyHints(newlineKeys, context)}: New line · ${formatKeyHint(
119
- "ctrl+c",
120
- context,
121
- )}: Clear · ${formatKeyHint("ctrl+r", context)}: Search history · ${formatKeyHint("shift+tab", context)}: Reasoning`;
145
+ parts.push(`${formatKeyHints(newlineKeys, context)}: New line`, `${formatKeyHint("ctrl+c", context)}: Clear`);
146
+ return `Type your message... ${parts.join(" · ")}`;
147
+ }
148
+
149
+ export function getDefaultComposerPlaceholder(
150
+ context: KeyDisplayContext = { platform: process.platform },
151
+ keybindings?: Pick<KeybindingsManager, "getDisplayString">,
152
+ ): string {
153
+ const effectiveKeybindings =
154
+ keybindings ??
155
+ KeybindingsManager.inMemory({
156
+ "app.message.queue": defaultMessageQueueKeysForPlatform(context.platform),
157
+ });
158
+ return buildComposerPlaceholder(effectiveKeybindings, context, { busy: false, busyPromptMode: "steer" });
122
159
  }
123
160
 
124
161
  export const DEFAULT_COMPOSER_PLACEHOLDER = getDefaultComposerPlaceholder();
162
+
163
+ export function getComposerPlaceholder(
164
+ keybindings: Pick<KeybindingsManager, "getDisplayString">,
165
+ context: KeyDisplayContext,
166
+ options: { readonly busy: boolean; readonly busyPromptMode: "steer" | "queue" },
167
+ ): string {
168
+ return buildComposerPlaceholder(keybindings, context, options);
169
+ }
125
170
  const WELCOME_RESERVED_CONTAINER_CHILD_LIMIT = 8;
171
+ const COMPOSER_RIGHT_GUTTER_WIDTH = 1;
126
172
 
127
173
  const IRC_SIDEBAR_TOGGLE_SHADOWING_ACTIONS: readonly AppKeybinding[] = [
128
174
  "app.plan.toggle",
@@ -169,7 +215,7 @@ function configureDefaultComposerChrome(editor: CustomEditor): void {
169
215
  editor.setInputPrefix(getDefaultInputPrefix());
170
216
  editor.setPlaceholder(getDefaultComposerPlaceholder());
171
217
  editor.setPaddingX(1);
172
- editor.setRightGutterWidth(1);
218
+ editor.setRightGutterWidth(COMPOSER_RIGHT_GUTTER_WIDTH);
173
219
  editor.setTopBorder(undefined);
174
220
  }
175
221
 
@@ -370,6 +416,9 @@ export class InteractiveMode implements InteractiveModeContext {
370
416
  #sttController: SttModeController | undefined;
371
417
  #resizeHandler?: () => void;
372
418
  #observerRegistry: SessionObserverRegistry;
419
+ #viewportOutputRevision = 0n;
420
+ #viewportOutputIdentity: string;
421
+
373
422
  #transcriptRegistry = new TranscriptItemRegistry();
374
423
 
375
424
  /** Direct controller capabilities for consumers that coordinate mode transitions. */
@@ -438,6 +487,8 @@ export class InteractiveMode implements InteractiveModeContext {
438
487
  this.mcpManager = mcpManager;
439
488
  this.#eventBus = eventBus;
440
489
  this.#keyDisplayContext = keyDisplayContext;
490
+ this.#viewportOutputIdentity = `session:${this.sessionManager.getSessionId()}`;
491
+
441
492
  this.keybindings.setDisplayContext(this.#keyDisplayContext);
442
493
  const thisMode = this;
443
494
  this.#goalModeController = new GoalModeController({
@@ -510,6 +561,14 @@ export class InteractiveMode implements InteractiveModeContext {
510
561
 
511
562
  this.ui = new TUI(new ProcessTerminal(), settings.get("showHardwareCursor"), {
512
563
  enableMouse: settings.get("mouse.enabled"),
564
+ copySelection: async text => {
565
+ try {
566
+ await copyToClipboard(text);
567
+ this.showStatus("Selection copied to clipboard");
568
+ } catch (error) {
569
+ this.showError(`Failed to copy selection: ${error instanceof Error ? error.message : String(error)}`);
570
+ }
571
+ },
513
572
  });
514
573
  this.ui.setClearOnShrink(settings.get("clearOnShrink"));
515
574
  this.chatContainer = new Container();
@@ -660,6 +719,7 @@ export class InteractiveMode implements InteractiveModeContext {
660
719
  getViewportRows: () => this.ui.terminal.rows,
661
720
  getReservedBottomRows: getWelcomeReservedBottomRows,
662
721
  changelogMarkdown: this.#changelogMarkdown,
722
+ rightGutterWidth: COMPOSER_RIGHT_GUTTER_WIDTH,
663
723
  collapseChangelog: settings.get("collapseChangelog"),
664
724
  keyDisplayContext: this.#keyDisplayContext,
665
725
  },
@@ -683,6 +743,11 @@ export class InteractiveMode implements InteractiveModeContext {
683
743
  this.ui.addChild(this.hookWidgetContainerBelow);
684
744
  this.ui.setBottomPinnedComponent(this.statusLine);
685
745
  this.ui.setFocus(this.editor);
746
+ this.ui.setViewportOutputSource({
747
+ identity: this.#viewportOutputIdentity,
748
+ revision: this.#viewportOutputRevision,
749
+ });
750
+
686
751
  this.petWidget?.dispose();
687
752
  this.petWidget = this.#createPetWidget(this.editor);
688
753
  const configuredPetMode = settings.get("pet.mode");
@@ -715,6 +780,12 @@ export class InteractiveMode implements InteractiveModeContext {
715
780
 
716
781
  this.#inputController.setupKeyHandlers();
717
782
  this.#inputController.setupEditorSubmitHandler();
783
+ this.editor.onViewportPageScroll = direction => {
784
+ this.ui.scrollViewportPages(direction);
785
+ };
786
+ this.editor.onViewportFollowLive = () => {
787
+ this.ui.followLiveViewport();
788
+ };
718
789
 
719
790
  // Wire observer registry to EventBus
720
791
  if (this.#eventBus) {
@@ -836,6 +907,7 @@ export class InteractiveMode implements InteractiveModeContext {
836
907
  // Set up theme file watcher
837
908
  onThemeChange(() => {
838
909
  clearRenderCache();
910
+ this.#ircSplitView.invalidateTheme();
839
911
  configureDefaultComposerChrome(this.editor);
840
912
  this.editor.setPlaceholder(this.#getComposerPlaceholder());
841
913
  this.ui.invalidate();
@@ -1056,27 +1128,11 @@ export class InteractiveMode implements InteractiveModeContext {
1056
1128
  return this.session.isStreaming || this.session.isCompacting;
1057
1129
  }
1058
1130
 
1059
- #getFirstKeyForAction(action: AppKeybinding): string | undefined {
1060
- return this.keybindings.getKeys(action)[0];
1061
- }
1062
-
1063
- #getMessageQueueShortcut(): string | undefined {
1064
- const preferredAction: AppKeybinding =
1065
- process.platform === "darwin" ? "app.message.followUp" : "app.message.queue";
1066
- const fallbackAction: AppKeybinding =
1067
- process.platform === "darwin" ? "app.message.queue" : "app.message.followUp";
1068
- return this.#getFirstKeyForAction(preferredAction) ?? this.#getFirstKeyForAction(fallbackAction);
1069
- }
1070
-
1071
1131
  #getComposerPlaceholder(): string {
1072
- const defaultPlaceholder = getDefaultComposerPlaceholder(this.#keyDisplayContext);
1073
- if (!this.#isPromptDeliveryBusy()) return defaultPlaceholder;
1074
- const submitAction = this.settings.get("busyPromptMode") === "steer" ? "Steer" : "Queue";
1075
- const submitKey = this.keybindings.getDisplayString("tui.input.submit", this.#keyDisplayContext);
1076
- const parts = submitKey ? [`${submitKey}: ${submitAction}`] : [];
1077
- const queueKey = this.#getMessageQueueShortcut();
1078
- if (queueKey) parts.push(`${formatKeyHint(queueKey, this.#keyDisplayContext)}: Queue`);
1079
- return parts.length > 0 ? `${defaultPlaceholder} · ${parts.join(" · ")}` : defaultPlaceholder;
1132
+ return getComposerPlaceholder(this.keybindings, this.#keyDisplayContext, {
1133
+ busy: this.#isPromptDeliveryBusy(),
1134
+ busyPromptMode: this.settings.get("busyPromptMode"),
1135
+ });
1080
1136
  }
1081
1137
 
1082
1138
  #getWelcomeReservedRows(width: number): number {
@@ -1093,6 +1149,7 @@ export class InteractiveMode implements InteractiveModeContext {
1093
1149
  this.statusLine,
1094
1150
  this.hookWidgetContainerAbove,
1095
1151
  this.editorContainer,
1152
+ this.petFloorContainer,
1096
1153
  this.hookWidgetContainerBelow,
1097
1154
  ].reduce((rows, component) => rows + component.render(width).length, 0);
1098
1155
 
@@ -1160,8 +1217,22 @@ export class InteractiveMode implements InteractiveModeContext {
1160
1217
  this.ui.requestRender();
1161
1218
  return false;
1162
1219
  }
1220
+ if (!settings.canWriteDurableConfig()) {
1221
+ this.showError(
1222
+ "Cannot change settings while config.yml has invalid YAML syntax. Repair config.yml and reload settings.",
1223
+ );
1224
+ return false;
1225
+ }
1226
+ try {
1227
+ settings.set("pet.mode", mode);
1228
+ } catch (error) {
1229
+ if (!settings.canWriteDurableConfig()) {
1230
+ this.showError(error instanceof Error ? error.message : String(error));
1231
+ return false;
1232
+ }
1233
+ throw error;
1234
+ }
1163
1235
  apply(mode);
1164
- settings.set("pet.mode", mode);
1165
1236
  this.ui.requestRender();
1166
1237
  return true;
1167
1238
  }
@@ -1460,6 +1531,12 @@ export class InteractiveMode implements InteractiveModeContext {
1460
1531
 
1461
1532
  this.#inputController.setupKeyHandlers();
1462
1533
  this.#inputController.setupEditorSubmitHandler();
1534
+ this.editor.onViewportPageScroll = direction => {
1535
+ this.ui.scrollViewportPages(direction);
1536
+ };
1537
+ this.editor.onViewportFollowLive = () => {
1538
+ this.ui.followLiveViewport();
1539
+ };
1463
1540
 
1464
1541
  void this.refreshSlashCommandState().catch(error => {
1465
1542
  logger.warn("Failed to refresh slash command state for custom editor", { error: String(error) });
@@ -1610,6 +1687,21 @@ export class InteractiveMode implements InteractiveModeContext {
1610
1687
  return this.#uiHelpers.isKnownSlashCommand(text);
1611
1688
  }
1612
1689
 
1690
+ /** Advances the sticky-viewport source only for semantic transcript output. */
1691
+ recordVisibleTranscriptMutation(): void {
1692
+ const identity = `session:${this.sessionManager.getSessionId()}`;
1693
+ if (identity !== this.#viewportOutputIdentity) {
1694
+ this.#viewportOutputIdentity = identity;
1695
+ this.#viewportOutputRevision = 0n;
1696
+ } else {
1697
+ this.#viewportOutputRevision += 1n;
1698
+ }
1699
+ this.ui.setViewportOutputSource({
1700
+ identity: this.#viewportOutputIdentity,
1701
+ revision: this.#viewportOutputRevision,
1702
+ });
1703
+ }
1704
+
1613
1705
  addMessageToChat(message: AgentMessage, options?: { populateHistory?: boolean }): Component[] {
1614
1706
  return this.#uiHelpers.addMessageToChat(message, options);
1615
1707
  }
@@ -2137,12 +2229,18 @@ export class InteractiveMode implements InteractiveModeContext {
2137
2229
  }
2138
2230
 
2139
2231
  resetIrcSidebarSession(): void {
2140
- this.ircLedger.reset();
2232
+ const sidebarVisible = this.#ircSplitView.effectiveSidebarVisible(this.ui.terminal.columns);
2233
+ const rightWidth = computeIrcSplitWidths(this.ui.terminal.columns).rightWidth;
2234
+ const beforeToken = sidebarVisible ? getIrcSidebarSemanticToken(this.ircLedger, rightWidth) : "";
2235
+ this.ircLedger.reset({ retireCurrentSessionIdentities: true });
2236
+ this.#ircSplitView.resetSource();
2237
+ const afterToken = sidebarVisible ? getIrcSidebarSemanticToken(this.ircLedger, rightWidth) : "";
2141
2238
  this.#eventController.resetIrcObservations();
2142
2239
  this.#ircSidebarRequestedVisible = false;
2143
2240
  this.#ircSplitView.setVisible(false);
2144
2241
  this.#uiHelpers.resetIrcSidebarHint();
2145
2242
  this.#syncIrcSidebarAvailabilityFromSettings();
2243
+ if (sidebarVisible && beforeToken !== afterToken) this.recordVisibleTranscriptMutation();
2146
2244
  }
2147
2245
 
2148
2246
  #invalidateIrcSidebarRender(): void {
@@ -4,9 +4,26 @@ import type { ParsedIrcMessage } from "./utils/irc-message";
4
4
 
5
5
  type InlineMode = "persistent" | "ephemeral";
6
6
 
7
- const MAX_RECORDS = 10_000;
8
- const MAX_RETAINED_UTF8_BYTES = 16 * 1024 * 1024;
9
- const MAX_SEEN_IDENTITIES = 100_000;
7
+ export const IRC_OBSERVATION_LEDGER_MAX_RECORDS = 10_000;
8
+ export const IRC_OBSERVATION_LEDGER_MAX_RETAINED_UTF8_BYTES = 16 * 1024 * 1024;
9
+ export const IRC_OBSERVATION_LEDGER_MAX_SEEN_IDENTITIES = 100_000;
10
+
11
+ export const __ircLedgerPerfCounters = {
12
+ enabled: false,
13
+ epochAdvances: 0,
14
+ snapshot() {
15
+ return { epochAdvances: this.epochAdvances };
16
+ },
17
+ enable(): void {
18
+ this.enabled = true;
19
+ },
20
+ disable(): void {
21
+ this.enabled = false;
22
+ },
23
+ reset(): void {
24
+ this.epochAdvances = 0;
25
+ },
26
+ };
10
27
 
11
28
  export type IrcObservationRecord = Readonly<
12
29
  ParsedIrcMessage & {
@@ -22,7 +39,7 @@ function measureRetainedUtf8Bytes(message: ParsedIrcMessage): number {
22
39
  let bytes = 4;
23
40
  for (const value of [message.observationId, message.from, message.to, message.text, message.kind]) {
24
41
  bytes += Buffer.byteLength(value, "utf8");
25
- if (bytes > MAX_RETAINED_UTF8_BYTES) return bytes;
42
+ if (bytes > IRC_OBSERVATION_LEDGER_MAX_RETAINED_UTF8_BYTES) return bytes;
26
43
  }
27
44
  return bytes;
28
45
  }
@@ -43,12 +60,28 @@ export class IrcObservationLedger {
43
60
  #nextSequence = 0;
44
61
  #seenObservationIdentities = new Set<string>();
45
62
  #identityCapacityExhausted = false;
63
+ #retiredSessionIdentities = new Set<string>();
46
64
  #evictedObservationIds = new Set<string>();
65
+ #mutationEpoch = 0;
66
+
67
+ get mutationEpoch(): number {
68
+ return this.#mutationEpoch;
69
+ }
70
+
71
+ #advanceMutationEpoch(): void {
72
+ this.#mutationEpoch++;
73
+ if (__ircLedgerPerfCounters.enabled) __ircLedgerPerfCounters.epochAdvances++;
74
+ }
47
75
 
48
76
  #rememberObservationIdentity(observationId: string): boolean {
49
77
  const identity = tombstoneIdentity(observationId);
50
- if (this.#seenObservationIdentities.has(identity) || this.#identityCapacityExhausted) return false;
51
- if (this.#seenObservationIdentities.size >= MAX_SEEN_IDENTITIES) {
78
+ if (
79
+ this.#seenObservationIdentities.has(identity) ||
80
+ this.#retiredSessionIdentities.has(identity) ||
81
+ this.#identityCapacityExhausted
82
+ )
83
+ return false;
84
+ if (this.#seenObservationIdentities.size >= IRC_OBSERVATION_LEDGER_MAX_SEEN_IDENTITIES) {
52
85
  this.#identityCapacityExhausted = true;
53
86
  return false;
54
87
  }
@@ -56,6 +89,18 @@ export class IrcObservationLedger {
56
89
  return true;
57
90
  }
58
91
 
92
+ #retireCurrentSessionIdentities(): void {
93
+ for (const identity of this.#seenObservationIdentities) {
94
+ this.#retiredSessionIdentities.delete(identity);
95
+ this.#retiredSessionIdentities.add(identity);
96
+ while (this.#retiredSessionIdentities.size > IRC_OBSERVATION_LEDGER_MAX_SEEN_IDENTITIES) {
97
+ const oldest = this.#retiredSessionIdentities.values().next().value;
98
+ if (oldest === undefined) break;
99
+ this.#retiredSessionIdentities.delete(oldest);
100
+ }
101
+ }
102
+ }
103
+
59
104
  #evict(observationId: string): void {
60
105
  const record = this.#records.get(observationId);
61
106
  if (!record) return;
@@ -65,7 +110,10 @@ export class IrcObservationLedger {
65
110
  }
66
111
 
67
112
  #enforceBounds(): void {
68
- while (this.#records.size > MAX_RECORDS || this.#retainedUtf8Bytes > MAX_RETAINED_UTF8_BYTES) {
113
+ while (
114
+ this.#records.size > IRC_OBSERVATION_LEDGER_MAX_RECORDS ||
115
+ this.#retainedUtf8Bytes > IRC_OBSERVATION_LEDGER_MAX_RETAINED_UTF8_BYTES
116
+ ) {
69
117
  const oldestObservationId = this.#records.keys().next().value;
70
118
  if (oldestObservationId === undefined) return;
71
119
  this.#evict(oldestObservationId);
@@ -76,10 +124,9 @@ export class IrcObservationLedger {
76
124
  observe(message: ParsedIrcMessage, panelVisibleAtObservation: boolean): IrcObservationRecord | undefined {
77
125
  const existing = this.#records.get(message.observationId);
78
126
  if (existing) return existing;
79
- if (!this.#rememberObservationIdentity(message.observationId)) return undefined;
80
-
81
127
  const estimatedBytes = measureRetainedUtf8Bytes(message);
82
- if (estimatedBytes > MAX_RETAINED_UTF8_BYTES) return undefined;
128
+ if (estimatedBytes > IRC_OBSERVATION_LEDGER_MAX_RETAINED_UTF8_BYTES) return undefined;
129
+ if (!this.#rememberObservationIdentity(message.observationId)) return undefined;
83
130
 
84
131
  const observedAt = Date.now();
85
132
  const mode: InlineMode = panelVisibleAtObservation ? "ephemeral" : "persistent";
@@ -94,6 +141,7 @@ export class IrcObservationLedger {
94
141
  this.#records.set(record.observationId, record);
95
142
  this.#retainedUtf8Bytes += estimatedBytes;
96
143
  this.#enforceBounds();
144
+ this.#advanceMutationEpoch();
97
145
  return this.#records.get(record.observationId);
98
146
  }
99
147
 
@@ -116,12 +164,20 @@ export class IrcObservationLedger {
116
164
  return observationIds;
117
165
  }
118
166
 
119
- reset(): void {
120
- for (const observationId of this.#records.keys()) {
121
- this.#evictedObservationIds.add(observationId);
167
+ reset(options?: { retireCurrentSessionIdentities?: boolean }): void {
168
+ const retainedRecordsChanged = this.#records.size > 0;
169
+ if (options?.retireCurrentSessionIdentities) this.#retireCurrentSessionIdentities();
170
+ else this.#retiredSessionIdentities.clear();
171
+ if (retainedRecordsChanged) {
172
+ for (const observationId of this.#records.keys()) {
173
+ this.#evictedObservationIds.add(observationId);
174
+ }
175
+ this.#records.clear();
176
+ this.#retainedUtf8Bytes = 0;
122
177
  }
123
- this.#records.clear();
124
- this.#retainedUtf8Bytes = 0;
125
178
  this.#nextSequence = 0;
179
+ this.#seenObservationIdentities.clear();
180
+ this.#identityCapacityExhausted = false;
181
+ if (retainedRecordsChanged) this.#advanceMutationEpoch();
126
182
  }
127
183
  }
@@ -115,6 +115,7 @@ export async function initializeExtensions(session: AgentSession, options: Initi
115
115
  compact: instructionsOrOptions => runExtensionCompact(session, instructionsOrOptions),
116
116
  clearContext: () => session.clearContext(),
117
117
  cycleModel: () => session.cycleModel(),
118
+ setModelProfile: name => session.activateModelProfileForControl(name),
118
119
  cycleThinkingLevel: () => session.cycleThinkingLevel(),
119
120
  setQueueMode: (kind, mode) => {
120
121
  if (kind === "steering" && (mode === "all" || mode === "one-at-a-time")) {
@@ -131,7 +132,7 @@ export async function initializeExtensions(session: AgentSession, options: Initi
131
132
  }
132
133
  return false;
133
134
  },
134
- invokeSkill: (name, args) => session.invokeSkill(name, args),
135
+ invokeSkill: (name, args, options) => session.invokeSkill(name, args, options),
135
136
  setPlanMode: on => session.setSdkPlanMode(on),
136
137
  operateGoal: (op, objective) => session.operateGoal(op, objective),
137
138
  getSkillState: () => session.skills.map(skill => ({ name: skill.name, description: skill.description })),
@@ -379,7 +380,24 @@ export async function initializeExtensions(session: AgentSession, options: Initi
379
380
  const disabled = [...(session.settings.get("disabledExtensions") ?? [])];
380
381
  const on = input.on === true;
381
382
  const next = on ? disabled.filter(value => value !== id) : [...new Set([...disabled, id])];
382
- session.settings.set("disabledExtensions", next);
383
+ if (!session.settings.canWriteDurableConfig()) {
384
+ throw Object.assign(
385
+ new Error(
386
+ "Cannot change settings while config.yml has invalid YAML syntax. Repair config.yml and reload settings.",
387
+ ),
388
+ { code: "invalid_request" },
389
+ );
390
+ }
391
+ try {
392
+ session.settings.set("disabledExtensions", next);
393
+ } catch (error) {
394
+ if (!session.settings.canWriteDurableConfig()) {
395
+ throw Object.assign(new Error(error instanceof Error ? error.message : String(error)), {
396
+ code: "invalid_request",
397
+ });
398
+ }
399
+ throw error;
400
+ }
383
401
  return { changed: true, enabled: on };
384
402
  }
385
403
  case "session.delete":
@@ -248,6 +248,8 @@ export interface InteractiveModeContext {
248
248
  getAssistantViewportAnchorId?(message: AssistantMessage): string;
249
249
  findLastAssistantMessage(): AssistantMessage | undefined;
250
250
  extractAssistantText(message: AssistantMessage): string;
251
+ /** Records one semantic visible-transcript mutation for the sticky viewport. */
252
+ recordVisibleTranscriptMutation?(): void;
251
253
  updateEditorTopBorder(): void;
252
254
  updateEditorBorderColor(): void;
253
255
  rebuildChatFromMessages(policy: TranscriptRebuildPolicy): void;