@openchambery/web 1.19.17-beta.2 → 1.19.17-beta.3

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 (128) hide show
  1. package/dist/assets/AgentAvatar-CUDJ3_nZ.js +1 -0
  2. package/dist/assets/AssistantView-CDXE0eTS.js +3 -0
  3. package/dist/assets/DiagramView-BrRV4o5T.js +1 -0
  4. package/dist/assets/FadeInOnReveal-BLSnyG2Z.js +1 -0
  5. package/dist/assets/Icon-C9ZHbwqp.js +1 -0
  6. package/dist/assets/MarkdownRenderer-CAdfUgV1.js +2 -0
  7. package/dist/assets/{MarkdownRendererImpl-BJd1Vjsc.js → MarkdownRendererImpl-D2dIlX0h.js} +2 -2
  8. package/dist/assets/MarkstreamRendererImpl-Drb2Tpv-.js +3 -0
  9. package/dist/assets/MobileDetailNavigation-BgCoDqtp.js +1 -0
  10. package/dist/assets/MobileShareBridge-ZlVURYWR.js +4 -0
  11. package/dist/assets/MobileSurface-DhLOTW0-.js +2 -0
  12. package/dist/assets/MultiRunWindow-BCvcwItK.js +1 -0
  13. package/dist/assets/OnboardingScreen-DSyZtiFS.js +1 -0
  14. package/dist/assets/OverlayScrollbar-C7fAOAKT.js +2 -0
  15. package/dist/assets/SettingsView-D57amZJb.js +25 -0
  16. package/dist/assets/SettingsWindow-B5DqLCkD.js +1 -0
  17. package/dist/assets/TerminalView-2_RFPaVs.js +8 -0
  18. package/dist/assets/{ToolOutputDialog-BQ_oXyS_.js → ToolOutputDialog-CQs_7KYT.js} +4 -4
  19. package/dist/assets/{appThemeRegistry-BDrIDWKH.js → appThemeRegistry-DpLe_jnW.js} +1 -1
  20. package/dist/assets/button-bhVnVYB_.js +1 -0
  21. package/dist/assets/collapsible-CoWJB_Zl.js +1 -0
  22. package/dist/assets/context-menu-gSO0b3sQ.js +1 -0
  23. package/dist/assets/diagram-Dx4UOZUn.js +1 -0
  24. package/dist/assets/dialog-C2Dw0yqA.js +1 -0
  25. package/dist/assets/{es-BQb6I8gl.js → es-Bxd4Lyu9.js} +2 -2
  26. package/dist/assets/{fileMentionTouchSelection-DzdgAvGS.js → fileMentionTouchSelection-CmQthb6Q.js} +2 -2
  27. package/dist/assets/{fr-BjGouC_l.js → fr-DiLDwVZs.js} +2 -2
  28. package/dist/assets/{gitApi-pLkl_IyQ.js → gitApi-8HxdR1zx.js} +3 -3
  29. package/dist/assets/input-8Te_vPwB.js +1 -0
  30. package/dist/assets/insertionBoundaries-Cdv9-1Bg.js +1 -0
  31. package/dist/assets/{ja-Cw9zliYY.js → ja-CWRlmEJM.js} +2 -2
  32. package/dist/assets/{ko-Dyz0Dk9b.js → ko-DmIugkgj.js} +2 -2
  33. package/dist/assets/main-CQY7JUX4.js +169 -0
  34. package/dist/assets/main-Cfru2-qr.js +2 -0
  35. package/dist/assets/{markdownHeightCache-BL8CUQ0M.js → markdownHeightCache-CT2d4yTV.js} +1 -1
  36. package/dist/assets/miniChat-BphFgAb_.js +2 -0
  37. package/dist/assets/mobile-BVRPTuLT.js +2 -0
  38. package/dist/assets/multirun-CYdieVzW.js +4 -0
  39. package/dist/assets/{pl-C7EY-dTH.js → pl-BU9AmdUM.js} +2 -2
  40. package/dist/assets/projectMeta-D8hiy3Dk.js +3 -0
  41. package/dist/assets/{pt-BR-B1uHpjFW.js → pt-BR-DQmEF9QB.js} +2 -2
  42. package/dist/assets/renderElectronMiniChatApp-CVqd3Nmc.js +2 -0
  43. package/dist/assets/renderMobileApp-DgtCGRKP.js +28 -0
  44. package/dist/assets/{runtimeConfig-BD1Fo5N5.js → runtimeConfig-DXXOBrK_.js} +1 -1
  45. package/dist/assets/runtimeEndpointReset-bSVOt7pi.js +147 -0
  46. package/dist/assets/sessionChangeSummary-CfnRZ0gS.js +2 -0
  47. package/dist/assets/sessionLookup-BZuujM6l.js +1 -0
  48. package/dist/assets/sortable-tabs-strip-BmvLnn6O.js +1 -0
  49. package/dist/assets/src-C_Yn9Ylz.css +2 -0
  50. package/dist/assets/{store-ClnJdJSj.js → store-BoixGrYS.js} +3 -3
  51. package/dist/assets/textarea-DRL916s6.js +1 -0
  52. package/dist/assets/tooltip-BLznq5KR.js +1 -0
  53. package/dist/assets/{uk-DgG8v2md.js → uk-Dax66sAC.js} +2 -2
  54. package/dist/assets/{useAppFontEffects-CN_3Jty7.js → useAppFontEffects-DhEfjuPp.js} +43 -43
  55. package/dist/assets/useDesktopWindowControlsLayout-BhNFU2Wd.js +1 -0
  56. package/dist/assets/{useEffectiveDirectory-C5Gg7EvP.js → useEffectiveDirectory-BhhzFJgX.js} +1 -1
  57. package/dist/assets/useMobileNavigationStore-1jdHeA49.js +413 -0
  58. package/dist/assets/useSessionAutoCleanup-C8u2-jrO.js +2 -0
  59. package/dist/assets/{useWorkerHighlightedLines-BSQQkZaO.js → useWorkerHighlightedLines-DFXK66lA.js} +2 -2
  60. package/dist/assets/vendor-zod-Dzk5kdUU.js +39 -0
  61. package/dist/assets/{zh-CN-C6OFKjeT.js → zh-CN-Du4VC8Z0.js} +2 -2
  62. package/dist/assets/{zh-TW-BVP99aMr.js → zh-TW-C0bY-Dso.js} +2 -2
  63. package/dist/index.html +3 -3
  64. package/dist/mini-chat.html +3 -3
  65. package/dist/mobile.html +3 -3
  66. package/package.json +1 -1
  67. package/server/index.js +51 -0
  68. package/server/lib/opencode/DOCUMENTATION.md +1 -0
  69. package/server/lib/opencode/core-routes.js +2 -0
  70. package/server/lib/opencode/feature-routes-runtime.js +20 -0
  71. package/server/lib/opencode/feature-routes-runtime.test.js +10 -0
  72. package/server/lib/opencode/routes.js +14 -1
  73. package/server/lib/opencode/settings-helpers.js +3 -0
  74. package/server/lib/opencode/settings-helpers.test.js +12 -0
  75. package/server/lib/opencode/shutdown-runtime.js +2 -0
  76. package/server/lib/question-auto-delegate/DOCUMENTATION.md +124 -0
  77. package/server/lib/question-auto-delegate/core.d.ts +255 -0
  78. package/server/lib/question-auto-delegate/core.js +1372 -0
  79. package/server/lib/question-auto-delegate/core.test.js +672 -0
  80. package/server/lib/question-auto-delegate/routes.js +223 -0
  81. package/server/lib/question-auto-delegate/routes.test.js +180 -0
  82. package/server/lib/question-auto-delegate/runtime.js +243 -0
  83. package/server/lib/session-goal/DOCUMENTATION.md +12 -2
  84. package/server/lib/session-goal/runtime.js +138 -35
  85. package/server/lib/session-goal/runtime.test.js +192 -0
  86. package/dist/assets/AgentAvatar-m1jNq4bX.js +0 -1
  87. package/dist/assets/AssistantView-DZMTfY1i.js +0 -3
  88. package/dist/assets/DiagramView-Byf2evGV.js +0 -1
  89. package/dist/assets/FadeInOnReveal-UGwClbL_.js +0 -1
  90. package/dist/assets/Icon-D1BcR8Tz.js +0 -1
  91. package/dist/assets/MarkdownRenderer-YMePGw0Z.js +0 -2
  92. package/dist/assets/MarkstreamRendererImpl-2oZKPfgd.js +0 -3
  93. package/dist/assets/MobileDetailNavigation-BeNUCxzK.js +0 -1
  94. package/dist/assets/MobileShareBridge-3tBtXXJJ.js +0 -4
  95. package/dist/assets/MobileSurface-ppn0zIpb.js +0 -2
  96. package/dist/assets/MultiRunWindow-CItY5bZ2.js +0 -1
  97. package/dist/assets/OnboardingScreen-bIWeMLVx.js +0 -1
  98. package/dist/assets/OverlayScrollbar-Bahg_P3r.js +0 -2
  99. package/dist/assets/SettingsView-0-Yqdzh-.js +0 -25
  100. package/dist/assets/SettingsWindow-1jOWzU2A.js +0 -1
  101. package/dist/assets/TerminalView-BLOCPFz9.js +0 -8
  102. package/dist/assets/button-9R-WgVMl.js +0 -1
  103. package/dist/assets/clipboard-GUUNlWIv.js +0 -1
  104. package/dist/assets/collapsible-Bbz9KiJf.js +0 -1
  105. package/dist/assets/context-menu-CSyOhRz8.js +0 -1
  106. package/dist/assets/diagram-NdsZih8P.js +0 -1
  107. package/dist/assets/dialog-DhMGbF_B.js +0 -1
  108. package/dist/assets/input-Cd_1HyQv.js +0 -1
  109. package/dist/assets/insertionBoundaries-DJvMDCJJ.js +0 -1
  110. package/dist/assets/main-B8fZfA3K.js +0 -169
  111. package/dist/assets/main-BGpPyUV2.js +0 -2
  112. package/dist/assets/miniChat-DHjphqgR.js +0 -2
  113. package/dist/assets/mobile-DNMX4u7x.js +0 -2
  114. package/dist/assets/multirun-BINJ2LCE.js +0 -4
  115. package/dist/assets/projectMeta-BMkGWqtz.js +0 -3
  116. package/dist/assets/renderElectronMiniChatApp-BonR3p7C.js +0 -2
  117. package/dist/assets/renderMobileApp-CZqNs2zK.js +0 -28
  118. package/dist/assets/runtimeEndpointReset-Dpx6GB_U.js +0 -147
  119. package/dist/assets/sessionChangeSummary-BG7l2heP.js +0 -2
  120. package/dist/assets/sessionLookup-BZuvc3Aa.js +0 -1
  121. package/dist/assets/sortable-tabs-strip-dYW6SH62.js +0 -1
  122. package/dist/assets/src-CO9q_82J.css +0 -2
  123. package/dist/assets/textarea-B9CmN13Z.js +0 -1
  124. package/dist/assets/tooltip-DSjr26A_.js +0 -1
  125. package/dist/assets/useDesktopWindowControlsLayout-C6kAkizT.js +0 -1
  126. package/dist/assets/useMobileNavigationStore-DDgESrzu.js +0 -413
  127. package/dist/assets/useSessionAutoCleanup-q2lauc7x.js +0 -2
  128. package/dist/assets/utils-2ZK8yboQ.js +0 -1
package/dist/index.html CHANGED
@@ -552,7 +552,7 @@
552
552
  opacity: 0.72;
553
553
  }
554
554
  </style>
555
- <script type="module" crossorigin src="/assets/main-BGpPyUV2.js"></script>
555
+ <script type="module" crossorigin src="/assets/main-Cfru2-qr.js"></script>
556
556
  <link rel="modulepreload" crossorigin href="/assets/rolldown-runtime-C0FnF6B9.js">
557
557
  <link rel="modulepreload" crossorigin href="/assets/runtime-auth-D66yq6ie.js">
558
558
  <link rel="modulepreload" crossorigin href="/assets/runtime-switch-LvWDU-SL.js">
@@ -572,8 +572,8 @@
572
572
  <link rel="modulepreload" crossorigin href="/assets/terminalApi-CfgOfnZk.js">
573
573
  <link rel="modulepreload" crossorigin href="/assets/types-BuiqMmh_.js">
574
574
  <link rel="modulepreload" crossorigin href="/assets/capgoAdapter-DCowZ2ab.js">
575
- <link rel="modulepreload" crossorigin href="/assets/runtimeConfig-BD1Fo5N5.js">
576
- <link rel="stylesheet" crossorigin href="/assets/src-CO9q_82J.css">
575
+ <link rel="modulepreload" crossorigin href="/assets/runtimeConfig-DXXOBrK_.js">
576
+ <link rel="stylesheet" crossorigin href="/assets/src-C_Yn9Ylz.css">
577
577
  </head>
578
578
  <body class="h-full bg-background text-foreground">
579
579
  <!-- Keep the startup layer outside #root. React replaces every child of
@@ -74,7 +74,7 @@
74
74
  }
75
75
  </style>
76
76
 
77
- <script type="module" crossorigin src="/assets/miniChat-DHjphqgR.js"></script>
77
+ <script type="module" crossorigin src="/assets/miniChat-BphFgAb_.js"></script>
78
78
  <link rel="modulepreload" crossorigin href="/assets/rolldown-runtime-C0FnF6B9.js">
79
79
  <link rel="modulepreload" crossorigin href="/assets/runtime-auth-D66yq6ie.js">
80
80
  <link rel="modulepreload" crossorigin href="/assets/runtime-switch-LvWDU-SL.js">
@@ -94,8 +94,8 @@
94
94
  <link rel="modulepreload" crossorigin href="/assets/terminalApi-CfgOfnZk.js">
95
95
  <link rel="modulepreload" crossorigin href="/assets/types-BuiqMmh_.js">
96
96
  <link rel="modulepreload" crossorigin href="/assets/capgoAdapter-DCowZ2ab.js">
97
- <link rel="modulepreload" crossorigin href="/assets/runtimeConfig-BD1Fo5N5.js">
98
- <link rel="stylesheet" crossorigin href="/assets/src-CO9q_82J.css">
97
+ <link rel="modulepreload" crossorigin href="/assets/runtimeConfig-DXXOBrK_.js">
98
+ <link rel="stylesheet" crossorigin href="/assets/src-C_Yn9Ylz.css">
99
99
  </head>
100
100
  <body class="h-full bg-background text-foreground">
101
101
  <div id="root" class="h-full">
package/dist/mobile.html CHANGED
@@ -63,7 +63,7 @@
63
63
  })();
64
64
  </script>
65
65
 
66
- <script type="module" crossorigin src="/assets/mobile-DNMX4u7x.js"></script>
66
+ <script type="module" crossorigin src="/assets/mobile-BVRPTuLT.js"></script>
67
67
  <link rel="modulepreload" crossorigin href="/assets/rolldown-runtime-C0FnF6B9.js">
68
68
  <link rel="modulepreload" crossorigin href="/assets/runtime-auth-D66yq6ie.js">
69
69
  <link rel="modulepreload" crossorigin href="/assets/runtime-switch-LvWDU-SL.js">
@@ -83,8 +83,8 @@
83
83
  <link rel="modulepreload" crossorigin href="/assets/terminalApi-CfgOfnZk.js">
84
84
  <link rel="modulepreload" crossorigin href="/assets/types-BuiqMmh_.js">
85
85
  <link rel="modulepreload" crossorigin href="/assets/capgoAdapter-DCowZ2ab.js">
86
- <link rel="modulepreload" crossorigin href="/assets/runtimeConfig-BD1Fo5N5.js">
87
- <link rel="stylesheet" crossorigin href="/assets/src-CO9q_82J.css">
86
+ <link rel="modulepreload" crossorigin href="/assets/runtimeConfig-DXXOBrK_.js">
87
+ <link rel="stylesheet" crossorigin href="/assets/src-C_Yn9Ylz.css">
88
88
  </head>
89
89
  <body class="h-full bg-background text-foreground">
90
90
  <div id="root" class="h-full"></div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openchambery/web",
3
- "version": "1.19.17-beta.2",
3
+ "version": "1.19.17-beta.3",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "main": "./server/index.js",
package/server/index.js CHANGED
@@ -86,6 +86,7 @@ import {
86
86
  } from './lib/notifications/live-activity-refresh-runtime.js';
87
87
  import { createNotificationTemplateRuntime } from './lib/notifications/template-runtime.js';
88
88
  import { createPermissionAutoAcceptRuntime } from './lib/permission-auto-accept/runtime.js';
89
+ import { createQuestionAutoDelegateRuntime } from './lib/question-auto-delegate/runtime.js';
89
90
  import { createGracefulShutdownRuntime } from './lib/opencode/shutdown-runtime.js';
90
91
  import { createProjectConfigRuntime } from './lib/projects/project-config.js';
91
92
  import { createRemoteClientAuthRuntime } from './lib/client-auth/remote-clients.js';
@@ -706,10 +707,25 @@ const sessionTitleRuntime = createSessionTitleRuntime({
706
707
  getSmallModelService,
707
708
  });
708
709
 
710
+ // Forward declaration: question auto-delegate is created after the event hub;
711
+ // goal hooks close over this binding so auto-delegate can keep goals alive.
712
+ let questionAutoDelegateRuntime = null;
713
+ /** Filled inside main() once the SQLite session index is constructed. */
714
+ let sessionIndexServiceRef = null;
715
+
709
716
  const sessionGoalRuntime = createSessionGoalRuntime({
710
717
  buildOpenCodeUrl,
711
718
  getOpenCodeAuthHeaders,
712
719
  getSmallModelService,
720
+ shouldKeepGoalActiveForQuestion: (sessionId) => (
721
+ questionAutoDelegateRuntime?.isAutoHandling(sessionId) === true
722
+ ),
723
+ isQuestionBlockingGoal: (sessionId) => (
724
+ questionAutoDelegateRuntime?.isBlockingSession(sessionId) === true
725
+ ),
726
+ onGoalPaused: (sessionId, directory) => (
727
+ questionAutoDelegateRuntime?.pauseForSessionTree?.(sessionId, directory)
728
+ ),
713
729
  emitGoalNotification: async ({ sessionId, directory, status, goal }) => {
714
730
  // The goal settle notification replaces the per-turn ready notifications
715
731
  // (suppressed while the goal is active) — so it obeys the same toggle.
@@ -765,6 +781,38 @@ notificationTriggerRuntime.setGetIsSessionAutoAccepting(
765
781
  (sessionId, directory) => permissionAutoAcceptRuntime.isSessionAutoAccepting(sessionId, directory),
766
782
  );
767
783
 
784
+ const broadcastQuestionAutoDelegateTip = createOpenChamberEventBroadcaster({
785
+ getOpenChamberEventClients: () => uiOpenChamberEventClients,
786
+ writeSseEvent,
787
+ });
788
+ questionAutoDelegateRuntime = createQuestionAutoDelegateRuntime({
789
+ globalEventHub: globalMessageStreamHub,
790
+ buildOpenCodeUrl,
791
+ getOpenCodeAuthHeaders,
792
+ readSettingsFromDiskMigrated,
793
+ sanitizeProjects,
794
+ listIndexedDirectories: async () => {
795
+ try {
796
+ const snap = sessionIndexServiceRef?.snapshot?.();
797
+ const directories = Array.isArray(snap?.directories) ? snap.directories : [];
798
+ return directories
799
+ .map((entry) => (typeof entry?.directory === 'string' ? entry.directory.trim() : ''))
800
+ .filter(Boolean);
801
+ } catch {
802
+ return [];
803
+ }
804
+ },
805
+ broadcastGlobalUiEvent,
806
+ broadcastOpenChamberEvent: broadcastQuestionAutoDelegateTip,
807
+ onUserTakeover: ({ sessionID, directory }) => {
808
+ // User pause / manual claim / feature disable → goal pause on root owner.
809
+ void sessionGoalRuntime.pauseForQuestion?.(sessionID, directory)?.catch((error) => {
810
+ console.warn('[question-auto-delegate] goal pause on takeover failed:', error?.message ?? error);
811
+ });
812
+ },
813
+ });
814
+ questionAutoDelegateRuntime.start();
815
+
768
816
  const openCodeWatcherRuntime = createOpenCodeWatcherRuntime({
769
817
  waitForOpenCodePort: (...args) => waitForOpenCodePort(...args),
770
818
  buildOpenCodeUrl,
@@ -1109,6 +1157,7 @@ const gracefulShutdownRuntime = createGracefulShutdownRuntime({
1109
1157
  openCodeWatcherRuntime,
1110
1158
  sessionTitleRuntime,
1111
1159
  sessionGoalRuntime,
1160
+ questionAutoDelegateRuntime,
1112
1161
  sessionRuntime,
1113
1162
  getHealthCheckInterval: () => healthCheckInterval,
1114
1163
  clearHealthCheckInterval: (value) => clearInterval(value),
@@ -1263,6 +1312,7 @@ async function main(options = {}) {
1263
1312
  dbPath: resolveSessionIndexDbPath({ sessionIndexDbPath }, OPENCHAMBER_DATA_DIR),
1264
1313
  getRuntimeConfig: () => getDesktopRuntimeConfig?.() ?? null,
1265
1314
  });
1315
+ sessionIndexServiceRef = sessionIndexService;
1266
1316
  const transcriptCacheService = createTranscriptCacheService({
1267
1317
  dbPath: resolveTranscriptCacheDbPath({ transcriptCacheDbPath: options.transcriptCacheDbPath }),
1268
1318
  });
@@ -1615,6 +1665,7 @@ async function main(options = {}) {
1615
1665
  broadcastGlobalUiEvent,
1616
1666
  writeSseEvent,
1617
1667
  permissionAutoAcceptRuntime,
1668
+ questionAutoDelegateRuntime,
1618
1669
  messageQueueService,
1619
1670
  messageQueueRuntime,
1620
1671
  globalMessageStreamHub,
@@ -192,6 +192,7 @@ This module provides OpenCode server integration utilities for the web server ru
192
192
  - `persistSettings(changes)`
193
193
  - `setRunExclusivePersist(fn)`
194
194
  - Persistent permission auto-accept policy is stored under `permissionAutoAccept`; execution ownership lives in `lib/permission-auto-accept/`.
195
+ - Question auto-delegate toggle is stored under `questionAutoDelegateEnabled` (default on); execution ownership lives in `lib/question-auto-delegate/` (shared `core.d.ts` for VS Code).
195
196
  - One-shot compact-chat defaults migration: when disk marker `compactChatDefaultsMigrationVersion` is missing, rewrite legacy/absent `chatRenderMode`/`activityRenderMode`/`showTurnChangedFiles` to `sorted`/`collapsed`/`true` and persist marker `1` (marker stays on disk; response allowlist still hides it). Marker already `1` preserves user values; `persistSettings` runs the same migration before the first write.
196
197
  - `startWebUiServer({ settingsPersistLock })` late-binds that shared runner after module load.
197
198
 
@@ -1193,6 +1193,8 @@ export const registerCommonRequestMiddleware = (app, dependencies) => {
1193
1193
  req.path.startsWith('/api/push') ||
1194
1194
  req.path.startsWith('/api/notifications') ||
1195
1195
  req.path.startsWith('/api/permission-auto-accept') ||
1196
+ req.path.startsWith('/api/question-auto-delegate') ||
1197
+ req.path.startsWith('/api/question') ||
1196
1198
  req.path.startsWith('/api/session-folders') ||
1197
1199
  req.path.startsWith('/api/small-model') ||
1198
1200
  req.path.startsWith('/api/goals') ||
@@ -7,6 +7,7 @@ import { registerGitRoutes } from '../git/routes.js';
7
7
  import { registerMagicPromptRoutes } from '../magic-prompts/routes.js';
8
8
  import { registerSessionFoldersRoutes } from '../session-folders/routes.js';
9
9
  import { registerPermissionAutoAcceptRoutes } from '../permission-auto-accept/runtime.js';
10
+ import { registerQuestionAutoDelegateRoutes } from '../question-auto-delegate/routes.js';
10
11
  import { registerConfigEntityRoutes } from './config-entity-routes.js';
11
12
  import { registerSettingsUtilityRoutes } from './core-routes.js';
12
13
  import { registerProjectIconRoutes } from './project-icon-routes.js';
@@ -123,6 +124,7 @@ export const createFeatureRoutesRuntime = (dependencies) => {
123
124
  getOpenChamberEventClients,
124
125
  writeSseEvent,
125
126
  permissionAutoAcceptRuntime,
127
+ questionAutoDelegateRuntime,
126
128
  messageQueueService,
127
129
  messageQueueRuntime,
128
130
  broadcastGlobalUiEvent,
@@ -140,6 +142,11 @@ export const createFeatureRoutesRuntime = (dependencies) => {
140
142
  });
141
143
 
142
144
  registerPermissionAutoAcceptRoutes(app, permissionAutoAcceptRuntime);
145
+ // Question auto-delegate + precise /api/question/:id/reply|reject intercepts
146
+ // must win before the generic OpenCode proxy.
147
+ if (questionAutoDelegateRuntime) {
148
+ registerQuestionAutoDelegateRoutes(app, questionAutoDelegateRuntime);
149
+ }
143
150
 
144
151
  registerOpenCodeRoutes(app, {
145
152
  crypto,
@@ -157,6 +164,19 @@ export const createFeatureRoutesRuntime = (dependencies) => {
157
164
  refreshOpenCodeAfterConfigChange,
158
165
  buildOpenCodeUrl,
159
166
  getOpenCodeAuthHeaders,
167
+ onSettingsPersisted: (updated, changes) => {
168
+ if (!questionAutoDelegateRuntime) return;
169
+ if (!Object.prototype.hasOwnProperty.call(changes ?? {}, 'questionAutoDelegateEnabled')) return;
170
+ // Only a real boolean may change runtime state — invalid/coerced junk is ignored.
171
+ const fromChanges = changes.questionAutoDelegateEnabled;
172
+ const fromUpdated = updated?.questionAutoDelegateEnabled;
173
+ const next = typeof fromUpdated === 'boolean'
174
+ ? fromUpdated
175
+ : (typeof fromChanges === 'boolean' ? fromChanges : null);
176
+ if (typeof next !== 'boolean') return;
177
+ // Persisted successfully — apply immediately. Failed saves never reach here.
178
+ questionAutoDelegateRuntime.applyEnabled(next);
179
+ },
160
180
  });
161
181
 
162
182
  registerProjectIconRoutes(app, {
@@ -43,6 +43,16 @@ describe('feature routes runtime composition', () => {
43
43
  expect(source).toContain('registerMessageQueueRoutes(app, { messageQueueService, messageQueueRuntime });');
44
44
  });
45
45
 
46
+ it('registers question auto-delegate routes (including reply/reject intercepts) before proxy composition', async () => {
47
+ const source = await fs.readFile(new URL('./feature-routes-runtime.js', import.meta.url), 'utf8');
48
+ expect(source).toContain("import { registerQuestionAutoDelegateRoutes } from '../question-auto-delegate/routes.js';");
49
+ expect(source).toMatch(/const \{[\s\S]*questionAutoDelegateRuntime,[\s\S]*\} = routeDependencies;/);
50
+ expect(source).toContain('registerQuestionAutoDelegateRoutes(app, questionAutoDelegateRuntime)');
51
+ const routesSource = await fs.readFile(new URL('../question-auto-delegate/routes.js', import.meta.url), 'utf8');
52
+ expect(routesSource).toContain("app.post('/api/question/:requestID/reply'");
53
+ expect(routesSource).toContain("app.post('/api/question/:requestID/reject'");
54
+ });
55
+
46
56
  it('registers session turn-page routes before proxy composition with OpenCode URL/auth deps', async () => {
47
57
  const source = await fs.readFile(new URL('./feature-routes-runtime.js', import.meta.url), 'utf8');
48
58
  expect(source).toContain("import { registerSessionTurnPageRoutes } from '../session-turn-pages/routes.js';");
@@ -21,6 +21,7 @@ export const registerOpenCodeRoutes = (app, dependencies) => {
21
21
  refreshOpenCodeAfterConfigChange,
22
22
  buildOpenCodeUrl,
23
23
  getOpenCodeAuthHeaders,
24
+ onSettingsPersisted,
24
25
  } = dependencies;
25
26
 
26
27
  let authLibrary = null;
@@ -328,7 +329,19 @@ export const registerOpenCodeRoutes = (app, dependencies) => {
328
329
 
329
330
  app.put('/api/config/settings', async (req, res) => {
330
331
  try {
331
- const updated = await persistSettings(req.body ?? {});
332
+ const changes = req.body ?? {};
333
+ const updated = await persistSettings(changes);
334
+ // Only after a successful persist — failed saves must not apply runtime flags.
335
+ if (
336
+ typeof onSettingsPersisted === 'function'
337
+ && Object.prototype.hasOwnProperty.call(changes, 'questionAutoDelegateEnabled')
338
+ ) {
339
+ try {
340
+ onSettingsPersisted(updated, changes);
341
+ } catch (error) {
342
+ console.warn('[API:PUT /api/config/settings] onSettingsPersisted failed:', error?.message ?? error);
343
+ }
344
+ }
332
345
  res.json(updated);
333
346
  } catch (error) {
334
347
  console.error('[API:PUT /api/config/settings] Failed to save settings:', error);
@@ -324,6 +324,9 @@ export const createSettingsHelpers = (dependencies) => {
324
324
  if (typeof candidate.sessionGoalEnabled === 'boolean') {
325
325
  result.sessionGoalEnabled = candidate.sessionGoalEnabled;
326
326
  }
327
+ if (typeof candidate.questionAutoDelegateEnabled === 'boolean') {
328
+ result.questionAutoDelegateEnabled = candidate.questionAutoDelegateEnabled;
329
+ }
327
330
  if (typeof candidate.sessionGoalDefaultBudgetEnabled === 'boolean') {
328
331
  result.sessionGoalDefaultBudgetEnabled = candidate.sessionGoalDefaultBudgetEnabled;
329
332
  }
@@ -135,6 +135,18 @@ describe('settings helpers', () => {
135
135
  });
136
136
  });
137
137
 
138
+ it('accepts questionAutoDelegateEnabled as a persisted boolean setting', () => {
139
+ const helpers = createTestHelpers();
140
+
141
+ expect(helpers.sanitizeSettingsUpdate({ questionAutoDelegateEnabled: true })).toEqual({
142
+ questionAutoDelegateEnabled: true,
143
+ });
144
+ expect(helpers.sanitizeSettingsUpdate({ questionAutoDelegateEnabled: false })).toEqual({
145
+ questionAutoDelegateEnabled: false,
146
+ });
147
+ expect(helpers.sanitizeSettingsUpdate({ questionAutoDelegateEnabled: 'true' })).toEqual({});
148
+ });
149
+
138
150
  it('accepts desktopUiPassword as a persisted shared setting', () => {
139
151
  const helpers = createTestHelpers();
140
152
 
@@ -10,6 +10,7 @@ export const createGracefulShutdownRuntime = (dependencies) => {
10
10
  sessionRuntime,
11
11
  sessionTitleRuntime,
12
12
  sessionGoalRuntime,
13
+ questionAutoDelegateRuntime,
13
14
  scheduledTasksRuntime,
14
15
  getHealthCheckInterval,
15
16
  clearHealthCheckInterval,
@@ -44,6 +45,7 @@ export const createGracefulShutdownRuntime = (dependencies) => {
44
45
  sessionRuntime.dispose();
45
46
  sessionTitleRuntime?.stop?.();
46
47
  sessionGoalRuntime?.stop?.();
48
+ questionAutoDelegateRuntime?.dispose?.();
47
49
  // Stop timers/queue only. Keep the process-lifetime run history store open so
48
50
  // in-flight attach/finalize can still write, and so exitProcess:false restarts
49
51
  // can reuse the same global singleton without reopening a closed SQLite handle.
@@ -0,0 +1,124 @@
1
+ # Question Auto-Delegate
2
+
3
+ ## Purpose
4
+
5
+ Server-owned automatic handling of OpenCode `question.asked` prompts. When
6
+ enabled (default), pending questions are answered after 30 seconds with a fixed
7
+ continuation text so agents (including background/subagent sessions) can proceed
8
+ without a connected UI. Manual reply/reject still use the official OpenCode
9
+ SDK shape; the Host only adds a single-flight claim so timer, manual, and
10
+ delegate paths cannot double-POST upstream.
11
+
12
+ Permission auto-accept is intentionally out of scope — this module never
13
+ replies to permission requests.
14
+
15
+ ## Settings
16
+
17
+ Persisted via existing `PUT /api/config/settings`:
18
+
19
+ | Field | Type | Default |
20
+ |---|---|---|
21
+ | `questionAutoDelegateEnabled` | `boolean` | `true` (absent ⇒ on) |
22
+
23
+ After a **successful** persist that includes a **real boolean** value, the Host
24
+ calls `applyEnabled`. Invalid/non-boolean input must not change runtime state.
25
+ Failed saves never call `applyEnabled`.
26
+
27
+ Boot/`start()` loads settings asynchronously with a **generation guard**: a
28
+ concurrent `applyEnabled` wins over a late disk read. Settings read failure
29
+ enters `unavailable` (`enabled=false`, `coverage.partial`) and **never** falls
30
+ back to auto-answering as if enabled.
31
+
32
+ - `false`: cancel timers; pending → `disabled`; notify goal takeover pause.
33
+ - `false → true`: still-pending requests restart a fresh 30s window (directory-bound only).
34
+ - repeated `true`: no-op (does not reset deadlines).
35
+ - Feature **off** still allows an explicit single-shot `delegate`.
36
+
37
+ Auto-submit requires **settings ready + enabled + authoritative directory**.
38
+
39
+ ## Architecture
40
+
41
+ | File | Role |
42
+ |---|---|
43
+ | `core.js` / `core.d.ts` | Platform-agnostic state machine (IO/timer injected). Shared contract for VS Code. |
44
+ | `runtime.js` | Web adapter: OpenCode upstream fetch, global event hub, settings, SSE tips, project + session-index directories. |
45
+ | `routes.js` | OpenChamber routes + precise `/api/question/:id/reply\|reject` intercepts. |
46
+
47
+ ## Snapshot
48
+
49
+ `GET /api/question-auto-delegate` returns the snapshot in `core.d.ts`.
50
+
51
+ Tip event (not a data payload):
52
+
53
+ ```ts
54
+ { type: 'openchamber:question-auto-delegate-changed', properties: { epoch, revision } }
55
+ ```
56
+
57
+ ## HTTP API
58
+
59
+ | Method | Path | Notes |
60
+ |---|---|---|
61
+ | `GET` | `/api/question-auto-delegate` | snapshot |
62
+ | `POST` | `.../requests/:id/pause` | `{ sessionID, directory, reason: 'interaction'\|'user' }` — bound identity conflicts → 409 |
63
+ | `POST` | `.../requests/:id/delegate` | recovers identity via scoped list; allowed when toggle off |
64
+ | `POST` | `/api/question/:id/reply` | manual: `answers` must be an array (incl. `[]`); never auto text |
65
+ | `POST` | `/api/question/:id/reject` | SDK body preserved |
66
+
67
+ Concurrent claim losers: HTTP **409** + `code: "question_submission_claimed"`.
68
+
69
+ ## Claim / upstream rules
70
+
71
+ 1. One authority owns at most one upstream POST per `requestID` (sync claim).
72
+ 2. **Definitive reject** (400/409/422) or pre-send throw → **release claim**, pause for human retry. **No immediate auto retry.**
73
+ 3. **Unclear post-dispatch result** (timeout/5xx/`uncertain`) → `uncertain` tombstone; no retry; still **blocks** goal.
74
+ 4. **Settled** must not be downgraded by a late POST failure.
75
+ 5. Successful **scoped** empty list may converge `uncertain`/pending → `external` settled **only for that directory**.
76
+ 6. Manual reply forwards `answers` as-is (including `[]`). Fixed text is auto/delegate only.
77
+ 7. Client `sessionID`/`directory` hints never override a bound identity (conflict → 409).
78
+ 8. Auto-submit requires an authoritative directory binding (from event, session cache, or `getSession` write-back). Missing directory → no timer.
79
+ 9. Unknown pause/delegate recovers via scoped `question.list` — never invents `questionCount=1`.
80
+
81
+ ## Reconcile
82
+
83
+ - Inventory = projects + session-index directories + observed request/event dirs.
84
+ - Track `successfulDirectories`; only those may settle missing requests.
85
+ - Unscoped list discovers unknowns only; never means “all directories empty”.
86
+ - Unscoped / directory-less pending triggers `ensureSessionLineage(sessionID, hint, requestID)`
87
+ so `getSession` can write the real worktree directory back and arm the 30s timer.
88
+ - Single-flight with trailing scope merge.
89
+ - Inventory/list failure → `coverage.partial`; prior state retained.
90
+
91
+ ## UpstreamResult (`core.d.ts`)
92
+
93
+ ```ts
94
+ { ok, uncertain?, notSent?, status, body }
95
+ ```
96
+
97
+ - `notSent: true` (compatible extension): request never left the process → release claim, pause for retry.
98
+ - Bare `status: 0` without `notSent` → `uncertain` (may have been sent). VS Code/web adapters that know the POST was never transmitted must set `notSent: true`.
99
+
100
+ ## Session goal linkage
101
+
102
+ - Auto-handling ask: clear idle timer only (goal stays `active`).
103
+ - `isBlockingSession` includes `uncertain` (and counting/paused/disabled/submitting).
104
+ - Manual claim / user pause / disable → `onUserTakeover` → goal `pauseForQuestion` (walks to **root owner**, uses owner directory).
105
+ - Goal abort/pause → `onGoalPaused` → `pauseForSessionTree` (pause related question timers, `pauseReason: 'goal'`).
106
+ - Continuation re-checks blocker immediately before `prompt_async`.
107
+
108
+ ## VS Code
109
+
110
+ `core.d.ts` is the shared contract. Compatible extensions in this fix:
111
+
112
+ - `readEnabled()` may return `null` (unavailable).
113
+ - `pauseForSessionTree` / `resolveRootSession` added.
114
+ - `applyEnabled` ignores non-boolean.
115
+ - Snapshot field set unchanged.
116
+
117
+ ## Tests
118
+
119
+ ```sh
120
+ bunx vitest run --project @openchamber/web \
121
+ packages/web/server/lib/question-auto-delegate/core.test.js \
122
+ packages/web/server/lib/question-auto-delegate/routes.test.js \
123
+ packages/web/server/lib/session-goal/runtime.test.js
124
+ ```