@gajae-code/coding-agent 0.11.11 → 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 (182) hide show
  1. package/CHANGELOG.md +49 -3
  2. package/dist/types/cli/read-cli.d.ts +1 -0
  3. package/dist/types/commands/deep-interview.d.ts +9 -0
  4. package/dist/types/commands/harness.d.ts +22 -0
  5. package/dist/types/commands/read.d.ts +6 -0
  6. package/dist/types/config/model-registry.d.ts +4 -0
  7. package/dist/types/config/provider-auth-health.d.ts +14 -0
  8. package/dist/types/config/provider-ranking.d.ts +59 -0
  9. package/dist/types/config/settings-schema.d.ts +29 -1
  10. package/dist/types/config/settings.d.ts +2 -0
  11. package/dist/types/extensibility/extensions/types.d.ts +22 -2
  12. package/dist/types/extensibility/shared-events.d.ts +6 -0
  13. package/dist/types/gjc-runtime/deep-interview-ambiguity.d.ts +5 -12
  14. package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +17 -32
  15. package/dist/types/gjc-runtime/deep-interview-stage.d.ts +37 -0
  16. package/dist/types/gjc-runtime/deep-interview-state.d.ts +4 -135
  17. package/dist/types/gjc-runtime/gc-runtime.d.ts +1 -1
  18. package/dist/types/gjc-runtime/memory-guard-owner-claims.d.ts +47 -0
  19. package/dist/types/gjc-runtime/ralplan-runtime.d.ts +52 -1
  20. package/dist/types/gjc-runtime/state-writer.d.ts +5 -46
  21. package/dist/types/gjc-runtime/team-launch.d.ts +1 -1
  22. package/dist/types/gjc-runtime/team-runtime.d.ts +2 -1
  23. package/dist/types/gjc-runtime/team-store.d.ts +12 -0
  24. package/dist/types/gjc-runtime/team-worker-memory-guard.d.ts +92 -0
  25. package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +7 -0
  26. package/dist/types/gjc-runtime/ultragoal-receipt-freshness.d.ts +8 -0
  27. package/dist/types/gjc-runtime/workflow-manifest.d.ts +3 -5
  28. package/dist/types/internal-urls/local-root-gc.d.ts +2 -0
  29. package/dist/types/main.d.ts +1 -1
  30. package/dist/types/memories/index.d.ts +2 -0
  31. package/dist/types/modes/acp/acp-agent.d.ts +8 -0
  32. package/dist/types/modes/components/assistant-message.d.ts +3 -1
  33. package/dist/types/modes/components/irc-sidebar.d.ts +41 -0
  34. package/dist/types/modes/components/read-tool-group.d.ts +4 -4
  35. package/dist/types/modes/components/session-observer-overlay.d.ts +4 -0
  36. package/dist/types/modes/components/settings-selector.d.ts +2 -0
  37. package/dist/types/modes/components/tool-execution.d.ts +8 -0
  38. package/dist/types/modes/controllers/event-controller.d.ts +3 -0
  39. package/dist/types/modes/interactive-mode.d.ts +2 -0
  40. package/dist/types/modes/irc-observation-ledger.d.ts +17 -1
  41. package/dist/types/modes/types.d.ts +2 -0
  42. package/dist/types/modes/utils/ui-helpers.d.ts +16 -0
  43. package/dist/types/runtime-mcp/smithery-auth.d.ts +5 -0
  44. package/dist/types/sdk/broker/lifecycle.d.ts +5 -0
  45. package/dist/types/sdk/bus/control-drain-lease.d.ts +52 -0
  46. package/dist/types/sdk/bus/index.d.ts +15 -1
  47. package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +37 -0
  48. package/dist/types/sdk/bus/prompt-reconciliation.d.ts +2 -1
  49. package/dist/types/sdk/bus/reconciliation-store.d.ts +65 -0
  50. package/dist/types/sdk/host/control/operations.d.ts +1 -1
  51. package/dist/types/sdk/host/host.d.ts +2 -0
  52. package/dist/types/sdk/host/query/handlers.d.ts +5 -0
  53. package/dist/types/session/agent-session.d.ts +44 -10
  54. package/dist/types/session/memory-guard-checkpoint-participant.d.ts +69 -0
  55. package/dist/types/session/session-manager.d.ts +25 -13
  56. package/dist/types/session/streaming-output.d.ts +60 -5
  57. package/dist/types/skill-state/active-state.d.ts +0 -4
  58. package/dist/types/skill-state/workflow-hud.d.ts +3 -0
  59. package/dist/types/tools/browser/launch.d.ts +7 -0
  60. package/dist/types/tools/fetch.d.ts +17 -1
  61. package/dist/types/tools/output-meta.d.ts +21 -1
  62. package/dist/types/tools/read-internals.d.ts +12 -0
  63. package/dist/types/tools/read.d.ts +45 -1
  64. package/dist/types/tools/resource-gc.d.ts +4 -0
  65. package/dist/types/tools/tool-result.d.ts +7 -1
  66. package/package.json +9 -13
  67. package/scripts/benchmark-sticky-viewport-pr1.ts +239 -0
  68. package/scripts/capture-sticky-viewport-showcase.ts +292 -0
  69. package/scripts/generate-sdk-operation-inventory.ts +2 -0
  70. package/scripts/verify-gjc-sdk-canonicalization.ts +122 -18
  71. package/scripts/verify-sticky-viewport-showcase.ts +611 -0
  72. package/src/capability/index.ts +8 -0
  73. package/src/cli/read-cli.ts +2 -1
  74. package/src/cli.ts +1 -0
  75. package/src/commands/deep-interview.ts +29 -9
  76. package/src/commands/harness.ts +35 -12
  77. package/src/commands/read.ts +11 -2
  78. package/src/commands/team.ts +1 -1
  79. package/src/config/model-registry.ts +21 -2
  80. package/src/config/provider-auth-health.ts +42 -0
  81. package/src/config/provider-ranking.ts +119 -0
  82. package/src/config/settings-schema.ts +23 -1
  83. package/src/config/settings.ts +246 -108
  84. package/src/defaults/gjc/skills/deep-interview/SKILL.md +229 -82
  85. package/src/defaults/gjc/skills/ralplan/SKILL.md +58 -25
  86. package/src/defaults/gjc/skills/team/SKILL.md +1 -1
  87. package/src/defaults/gjc/skills/ultragoal/SKILL.md +25 -15
  88. package/src/eval/py/executor.ts +7 -2
  89. package/src/extensibility/extensions/types.ts +28 -4
  90. package/src/extensibility/shared-events.ts +5 -0
  91. package/src/gjc-runtime/deep-interview-ambiguity.ts +47 -82
  92. package/src/gjc-runtime/deep-interview-recorder.ts +227 -310
  93. package/src/gjc-runtime/deep-interview-runtime.ts +109 -270
  94. package/src/gjc-runtime/deep-interview-stage.ts +1016 -0
  95. package/src/gjc-runtime/deep-interview-state.ts +12 -943
  96. package/src/gjc-runtime/gc-render.ts +1 -0
  97. package/src/gjc-runtime/gc-runtime.ts +11 -1
  98. package/src/gjc-runtime/memory-guard-owner-claims.ts +389 -0
  99. package/src/gjc-runtime/ralplan-runtime.ts +742 -85
  100. package/src/gjc-runtime/state-runtime.ts +2 -1
  101. package/src/gjc-runtime/state-writer.ts +70 -222
  102. package/src/gjc-runtime/team-launch.ts +34 -5
  103. package/src/gjc-runtime/team-runtime.ts +970 -33
  104. package/src/gjc-runtime/team-store.ts +32 -0
  105. package/src/gjc-runtime/team-worker-memory-guard.ts +363 -0
  106. package/src/gjc-runtime/team-workers.ts +1 -0
  107. package/src/gjc-runtime/tmux-owner-isolation.ts +18 -0
  108. package/src/gjc-runtime/ultragoal-receipt-freshness.ts +20 -0
  109. package/src/gjc-runtime/ultragoal-runtime.ts +13 -7
  110. package/src/gjc-runtime/workflow-manifest.generated.json +97 -350
  111. package/src/gjc-runtime/workflow-manifest.ts +34 -179
  112. package/src/hooks/native-skill-hook.ts +17 -4
  113. package/src/hooks/skill-state.ts +1 -23
  114. package/src/internal-urls/docs-index.generated.ts +8 -9
  115. package/src/internal-urls/local-root-gc.ts +127 -0
  116. package/src/main.ts +9 -5
  117. package/src/memories/index.ts +10 -0
  118. package/src/modes/DESIGN.md +47 -0
  119. package/src/modes/acp/acp-agent.ts +43 -7
  120. package/src/modes/acp/acp-event-mapper.ts +14 -7
  121. package/src/modes/acp/acp-mode.ts +39 -3
  122. package/src/modes/components/assistant-message.ts +123 -22
  123. package/src/modes/components/irc-sidebar.ts +176 -62
  124. package/src/modes/components/model-selector.ts +61 -4
  125. package/src/modes/components/oauth-selector.ts +42 -12
  126. package/src/modes/components/read-tool-group.ts +82 -14
  127. package/src/modes/components/session-observer-overlay.ts +187 -32
  128. package/src/modes/components/settings-selector.ts +60 -18
  129. package/src/modes/components/tool-execution.ts +138 -27
  130. package/src/modes/controllers/event-controller.ts +109 -27
  131. package/src/modes/controllers/extension-ui-controller.ts +19 -2
  132. package/src/modes/controllers/input-controller.ts +18 -3
  133. package/src/modes/controllers/selector-controller.ts +83 -37
  134. package/src/modes/interactive-mode.ts +62 -3
  135. package/src/modes/irc-observation-ledger.ts +71 -15
  136. package/src/modes/runtime-init.ts +19 -2
  137. package/src/modes/types.ts +2 -0
  138. package/src/modes/utils/ui-helpers.ts +94 -1
  139. package/src/prompts/agents/architect.md +9 -1
  140. package/src/prompts/agents/critic.md +9 -0
  141. package/src/prompts/system/system-prompt.md +0 -1
  142. package/src/prompts/tools/read.md +4 -2
  143. package/src/runtime-mcp/smithery-auth.ts +17 -2
  144. package/src/runtime-mcp/smithery-connect.ts +11 -1
  145. package/src/sdk/broker/lifecycle.ts +25 -6
  146. package/src/sdk/bus/control-drain-lease.ts +104 -0
  147. package/src/sdk/bus/index.ts +341 -38
  148. package/src/sdk/bus/kind-aware-reconciliation.ts +242 -0
  149. package/src/sdk/bus/prompt-reconciliation.ts +2 -1
  150. package/src/sdk/bus/reconciliation-store.ts +224 -0
  151. package/src/sdk/host/control/dispatch.ts +5 -1
  152. package/src/sdk/host/control/operations.ts +1 -1
  153. package/src/sdk/host/host.ts +23 -1
  154. package/src/sdk/host/query/handlers.ts +55 -0
  155. package/src/sdk/mcp/server.ts +46 -18
  156. package/src/sdk/protocol/operation-inventory.generated.json +29 -0
  157. package/src/sdk/protocol/operation-registry.ts +7 -2
  158. package/src/sdk/session.ts +13 -1
  159. package/src/session/agent-session.ts +466 -50
  160. package/src/session/memory-guard-checkpoint-participant.ts +131 -0
  161. package/src/session/session-manager.ts +764 -17
  162. package/src/session/streaming-output.ts +262 -43
  163. package/src/setup/provider-onboarding.ts +19 -4
  164. package/src/skill-state/active-state.ts +0 -10
  165. package/src/skill-state/workflow-hud.ts +25 -2
  166. package/src/skill-state/workflow-mutation-guard.ts +65 -7
  167. package/src/tools/ask.ts +89 -69
  168. package/src/tools/browser/launch.ts +41 -6
  169. package/src/tools/fetch.ts +114 -23
  170. package/src/tools/output-meta.ts +176 -9
  171. package/src/tools/path-utils.ts +5 -1
  172. package/src/tools/read-internals.ts +44 -0
  173. package/src/tools/read.ts +1542 -310
  174. package/src/tools/resource-gc.ts +182 -18
  175. package/src/tools/tool-result.ts +14 -1
  176. package/src/web/search/providers/codex.ts +1 -1
  177. package/dist/types/gjc-runtime/deep-interview-draft.d.ts +0 -8
  178. package/dist/types/gjc-runtime/deep-interview-payload.d.ts +0 -41
  179. package/dist/types/gjc-runtime/deep-interview-repair.d.ts +0 -17
  180. package/src/gjc-runtime/deep-interview-draft.ts +0 -843
  181. package/src/gjc-runtime/deep-interview-payload.ts +0 -262
  182. package/src/gjc-runtime/deep-interview-repair.ts +0 -1674
@@ -281,7 +281,24 @@ export class ExtensionUiController {
281
281
  const disabled = [...(session.settings.get("disabledExtensions") ?? [])];
282
282
  const on = input.on === true;
283
283
  const next = on ? disabled.filter(value => value !== id) : [...new Set([...disabled, id])];
284
- session.settings.set("disabledExtensions", next);
284
+ if (!session.settings.canWriteDurableConfig()) {
285
+ throw Object.assign(
286
+ new Error(
287
+ "Cannot change settings while config.yml has invalid YAML syntax. Repair config.yml and reload settings.",
288
+ ),
289
+ { code: "invalid_request" },
290
+ );
291
+ }
292
+ try {
293
+ session.settings.set("disabledExtensions", next);
294
+ } catch (error) {
295
+ if (!session.settings.canWriteDurableConfig()) {
296
+ throw Object.assign(new Error(error instanceof Error ? error.message : String(error)), {
297
+ code: "invalid_request",
298
+ });
299
+ }
300
+ throw error;
301
+ }
285
302
  return { changed: true, enabled: on };
286
303
  }
287
304
  case "session.delete":
@@ -442,7 +459,7 @@ export class ExtensionUiController {
442
459
  }
443
460
  return false;
444
461
  },
445
- invokeSkill: (name, args) => this.ctx.session.invokeSkill(name, args),
462
+ invokeSkill: (name, args, options) => this.ctx.session.invokeSkill(name, args, options),
446
463
  setPlanMode: on => this.ctx.session.setSdkPlanMode(on),
447
464
  operateGoal: (op, objective) => this.ctx.session.operateGoal(op, objective),
448
465
  getSkillState: () =>
@@ -2044,9 +2044,24 @@ export class InputController {
2044
2044
  }
2045
2045
 
2046
2046
  toggleThinkingBlockVisibility(): void {
2047
- this.ctx.hideThinkingBlock = !this.ctx.hideThinkingBlock;
2048
- settings.set("hideThinkingBlock", this.ctx.hideThinkingBlock);
2049
- this.ctx.session.agent.hideThinkingSummary = this.ctx.hideThinkingBlock;
2047
+ if (!settings.canWriteDurableConfig()) {
2048
+ this.ctx.showError(
2049
+ "Cannot change settings while config.yml has invalid YAML syntax. Repair config.yml and reload settings.",
2050
+ );
2051
+ return;
2052
+ }
2053
+ const hideThinkingBlock = !this.ctx.hideThinkingBlock;
2054
+ try {
2055
+ settings.set("hideThinkingBlock", hideThinkingBlock);
2056
+ } catch (error) {
2057
+ if (!settings.canWriteDurableConfig()) {
2058
+ this.ctx.showError(error instanceof Error ? error.message : String(error));
2059
+ return;
2060
+ }
2061
+ throw error;
2062
+ }
2063
+ this.ctx.hideThinkingBlock = hideThinkingBlock;
2064
+ this.ctx.session.setThinkingVisibility(hideThinkingBlock ? "hidden" : "visible");
2050
2065
 
2051
2066
  // Rebuild chat from session messages
2052
2067
  // Detach the live streaming component before the disposing clear() so the
@@ -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> {
@@ -61,7 +61,7 @@ import { GajaePetWidget, type PetMode } from "./components/gajae-pet-widget";
61
61
  import type { HookEditorComponent } from "./components/hook-editor";
62
62
  import type { HookInputComponent } from "./components/hook-input";
63
63
  import type { HookSelectorComponent } from "./components/hook-selector";
64
- import { IrcSplitViewComponent } from "./components/irc-sidebar";
64
+ import { computeIrcSplitWidths, getIrcSidebarSemanticToken, IrcSplitViewComponent } from "./components/irc-sidebar";
65
65
  import {
66
66
  getPetUnavailableWarning,
67
67
  isPetAvailable,
@@ -416,6 +416,9 @@ export class InteractiveMode implements InteractiveModeContext {
416
416
  #sttController: SttModeController | undefined;
417
417
  #resizeHandler?: () => void;
418
418
  #observerRegistry: SessionObserverRegistry;
419
+ #viewportOutputRevision = 0n;
420
+ #viewportOutputIdentity: string;
421
+
419
422
  #transcriptRegistry = new TranscriptItemRegistry();
420
423
 
421
424
  /** Direct controller capabilities for consumers that coordinate mode transitions. */
@@ -484,6 +487,8 @@ export class InteractiveMode implements InteractiveModeContext {
484
487
  this.mcpManager = mcpManager;
485
488
  this.#eventBus = eventBus;
486
489
  this.#keyDisplayContext = keyDisplayContext;
490
+ this.#viewportOutputIdentity = `session:${this.sessionManager.getSessionId()}`;
491
+
487
492
  this.keybindings.setDisplayContext(this.#keyDisplayContext);
488
493
  const thisMode = this;
489
494
  this.#goalModeController = new GoalModeController({
@@ -738,6 +743,11 @@ export class InteractiveMode implements InteractiveModeContext {
738
743
  this.ui.addChild(this.hookWidgetContainerBelow);
739
744
  this.ui.setBottomPinnedComponent(this.statusLine);
740
745
  this.ui.setFocus(this.editor);
746
+ this.ui.setViewportOutputSource({
747
+ identity: this.#viewportOutputIdentity,
748
+ revision: this.#viewportOutputRevision,
749
+ });
750
+
741
751
  this.petWidget?.dispose();
742
752
  this.petWidget = this.#createPetWidget(this.editor);
743
753
  const configuredPetMode = settings.get("pet.mode");
@@ -770,6 +780,12 @@ export class InteractiveMode implements InteractiveModeContext {
770
780
 
771
781
  this.#inputController.setupKeyHandlers();
772
782
  this.#inputController.setupEditorSubmitHandler();
783
+ this.editor.onViewportPageScroll = direction => {
784
+ this.ui.scrollViewportPages(direction);
785
+ };
786
+ this.editor.onViewportFollowLive = () => {
787
+ this.ui.followLiveViewport();
788
+ };
773
789
 
774
790
  // Wire observer registry to EventBus
775
791
  if (this.#eventBus) {
@@ -891,6 +907,7 @@ export class InteractiveMode implements InteractiveModeContext {
891
907
  // Set up theme file watcher
892
908
  onThemeChange(() => {
893
909
  clearRenderCache();
910
+ this.#ircSplitView.invalidateTheme();
894
911
  configureDefaultComposerChrome(this.editor);
895
912
  this.editor.setPlaceholder(this.#getComposerPlaceholder());
896
913
  this.ui.invalidate();
@@ -1132,6 +1149,7 @@ export class InteractiveMode implements InteractiveModeContext {
1132
1149
  this.statusLine,
1133
1150
  this.hookWidgetContainerAbove,
1134
1151
  this.editorContainer,
1152
+ this.petFloorContainer,
1135
1153
  this.hookWidgetContainerBelow,
1136
1154
  ].reduce((rows, component) => rows + component.render(width).length, 0);
1137
1155
 
@@ -1199,8 +1217,22 @@ export class InteractiveMode implements InteractiveModeContext {
1199
1217
  this.ui.requestRender();
1200
1218
  return false;
1201
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
+ }
1202
1235
  apply(mode);
1203
- settings.set("pet.mode", mode);
1204
1236
  this.ui.requestRender();
1205
1237
  return true;
1206
1238
  }
@@ -1499,6 +1531,12 @@ export class InteractiveMode implements InteractiveModeContext {
1499
1531
 
1500
1532
  this.#inputController.setupKeyHandlers();
1501
1533
  this.#inputController.setupEditorSubmitHandler();
1534
+ this.editor.onViewportPageScroll = direction => {
1535
+ this.ui.scrollViewportPages(direction);
1536
+ };
1537
+ this.editor.onViewportFollowLive = () => {
1538
+ this.ui.followLiveViewport();
1539
+ };
1502
1540
 
1503
1541
  void this.refreshSlashCommandState().catch(error => {
1504
1542
  logger.warn("Failed to refresh slash command state for custom editor", { error: String(error) });
@@ -1649,6 +1687,21 @@ export class InteractiveMode implements InteractiveModeContext {
1649
1687
  return this.#uiHelpers.isKnownSlashCommand(text);
1650
1688
  }
1651
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
+
1652
1705
  addMessageToChat(message: AgentMessage, options?: { populateHistory?: boolean }): Component[] {
1653
1706
  return this.#uiHelpers.addMessageToChat(message, options);
1654
1707
  }
@@ -2176,12 +2229,18 @@ export class InteractiveMode implements InteractiveModeContext {
2176
2229
  }
2177
2230
 
2178
2231
  resetIrcSidebarSession(): void {
2179
- 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) : "";
2180
2238
  this.#eventController.resetIrcObservations();
2181
2239
  this.#ircSidebarRequestedVisible = false;
2182
2240
  this.#ircSplitView.setVisible(false);
2183
2241
  this.#uiHelpers.resetIrcSidebarHint();
2184
2242
  this.#syncIrcSidebarAvailabilityFromSettings();
2243
+ if (sidebarVisible && beforeToken !== afterToken) this.recordVisibleTranscriptMutation();
2185
2244
  }
2186
2245
 
2187
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
  }
@@ -132,7 +132,7 @@ export async function initializeExtensions(session: AgentSession, options: Initi
132
132
  }
133
133
  return false;
134
134
  },
135
- invokeSkill: (name, args) => session.invokeSkill(name, args),
135
+ invokeSkill: (name, args, options) => session.invokeSkill(name, args, options),
136
136
  setPlanMode: on => session.setSdkPlanMode(on),
137
137
  operateGoal: (op, objective) => session.operateGoal(op, objective),
138
138
  getSkillState: () => session.skills.map(skill => ({ name: skill.name, description: skill.description })),
@@ -380,7 +380,24 @@ export async function initializeExtensions(session: AgentSession, options: Initi
380
380
  const disabled = [...(session.settings.get("disabledExtensions") ?? [])];
381
381
  const on = input.on === true;
382
382
  const next = on ? disabled.filter(value => value !== id) : [...new Set([...disabled, id])];
383
- 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
+ }
384
401
  return { changed: true, enabled: on };
385
402
  }
386
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;