synabun 2026.5.2-beta → 2026.5.3-beta

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  ## THIS IS A NEW CHANGELOG START FRESH ##
2
2
 
3
+ ## 2026-05-03
4
+
5
+ ### Fixed — OpenCode Sidepanel: Auto-Recovery for Kimi/OpenCode `question` Tool Deadlock
6
+
7
+ - **Symptom** — In the OpenCode sidepanel with `kimi-for-coding/k2p6` (and likely other non-Anthropic providers via OpenCode 1.14.30), answering an interactive question card (e.g. `/synabun changelog`'s Save/Edit/Cancel prompt, or any skill that calls the OpenCode `question` tool) hung the session indefinitely. Diagnostic from `~/.local/share/opencode/log/2026-05-01T235721.log` (session `ses_2144a6db9ffenIy1ILBw42mMeg` / question `que_debd75e030018zoXNsm4WhqaPC`): `service=question … asking` + `bus type=question.asked publishing` at 03:17:33 → user-answer `POST /question/<id>` returned **200 in 24 ms** at 03:21:16 → **12 minutes of total log silence** (no `service=llm providerID=kimi-for-coding modelID=k2p6 … stream` follow-up, no `tool.registry status=completed`, no bus events, no session.processor activity) → user clicked Stop at 03:33:23 (`session.prompt cancel` / `error=Aborted process`). OpenCode-side stall: HTTP layer accepted the answer but `session.processor` never resumed the LLM stream.
8
+ - **Fix — sidepanel-side watchdog + auto-recovery** in `neural-interface/public/shared/ocp/ocp-tabs.js`. Single-file change.
9
+ - **`QUESTION_REPLY_WATCHDOG_MS = 75000`** — generous enough for slow tool-follow-up TTFT (typically 1–3 s) but recovers fast enough that the user isn't burned (previous behavior: 12-min stall before manual Stop).
10
+ - **`buildAnswerSyntheticMessage(questions, answers)`** — converts the question card's `pendingAnswers` object into a human-readable string. Single answer → bare value; multi-answer → `Here are my answers:\n- Q: A\n…`; multi-select arrays joined with `, `.
11
+ - **`scheduleQuestionReplyWatchdog(tab, syntheticMessage)`** — sets `tab._questionReplyWatchdogId` setTimeout + caches the synthetic on `tab._questionReplyAnswerSnapshot`. Both transient (NOT in `saveTabs` serialization list — auto-discarded on reload). Idempotent: re-scheduling clears the prior timer.
12
+ - **`clearQuestionReplyWatchdog(tab)`** — idempotent timer-clear + snapshot reset. Wired into 4 sites: `closeTab`, `sendMessage` start (after `clearPlanFinalizationWatchdog`), `abortTab` (after `clearPlanFinalizationWatchdog`), and `handleSSEEvent` immediately after `tab._lastEventAt = Date.now()` — that single insertion catches every "LLM is alive" signal (`message.part.delta`, `message.updated`, `message.completed`, `session.idle`, `session.error`, `tool.start`, `tool.result`, etc.) without per-handler hooks.
13
+ - **`executeQuestionReplyRecovery(tab, syntheticMessage)`** async — switches back to the originating tab if the user navigated away (`_tabs.indexOf(tab)` + `switchTab(idx)` since `sendMessage` operates on `activeTab()`). Renders banner ("Model didn't resume after answering. Auto-recovering — resending your answer as a regular message…"), rejects pending send request, resets `running`/`turnStartedAt`/turn state, then `await requestWs('message:abort', { sessionId }, 5000)` to settle the abort server-side BEFORE `await sendMessage(syntheticMessage)` — prevents a late `session.idle` from the abort racing into the new turn and flipping `tab.running` back to false.
14
+ - **Wired into `renderOpencodeQuestion`'s `question:reply` `.then(resp)`** success branch — schedules watchdog only when `resp.status < 400` (don't watchdog a failed reply). Anthropic / Qwen flows emit `message.part.delta` within ~1 s of `question:reply`, clearing the watchdog before it ever fires; only the Kimi-style stall reaches the recovery path.
15
+
16
+ ### Fixed — OpenCode Sidepanel MCP Profile Dropdown Label Sync
17
+
18
+ - **`populateProfileDropdown()` missing label update** — `neural-interface/public/shared/ocp/ocp-panel.js:1405`. When the `mcp:profile-changed` event fired (e.g., from MCP profile action='set'), the dropdown menu items were rebuilt with correct selection state, but the visible label itself was never updated. This left the OpenCode sidepanel showing a stale profile name while the MCP profile had changed.
19
+ - **Fix** — Added 4 lines inside `populateProfileDropdown(dd)` immediately before the `for (const p of _mcpProfiles)` rebuild loop: `const matched = _mcpProfiles.find(p => p.id === _currentProfile); lbl.textContent = matched ? matched.label : _currentProfile; dd.classList.add('has-value'); dd._value = _currentProfile;`. The dropdown label now stays in sync with profile changes via the MCP tool, sidepanel API, or file reload.
20
+
3
21
  ## 2026-05-02
4
22
 
5
23
  ### Fixed — OpenCode Sidepanel: Read-Only Tools After Plan→Build / Plan→Chat Transition (Qwen + non-Anthropic models)
@@ -1405,6 +1405,10 @@ function populateProfileDropdown(dd) {
1405
1405
  const lbl = dd.querySelector('.ocp-dd-label');
1406
1406
  if (!menu || !lbl) return;
1407
1407
  menu.innerHTML = '';
1408
+ const matched = _mcpProfiles.find(p => p.id === _currentProfile);
1409
+ lbl.textContent = matched ? matched.label : _currentProfile;
1410
+ dd.classList.add('has-value');
1411
+ dd._value = _currentProfile;
1408
1412
  for (const p of _mcpProfiles) {
1409
1413
  const opt = document.createElement('div');
1410
1414
  opt.className = 'ocp-dd-option' + (p.id === _currentProfile ? ' selected' : '');
@@ -669,6 +669,8 @@ export function switchTab(idx) {
669
669
  export function closeTab(idx) {
670
670
  if (idx < 0 || idx >= _tabs.length) return;
671
671
  const tab = _tabs[idx];
672
+ clearPlanFinalizationWatchdog(tab);
673
+ clearQuestionReplyWatchdog(tab);
672
674
  flushSessionSnapshotSave(tab);
673
675
  if (tab.pillEl) tab.pillEl.remove();
674
676
  if (tab.trayPillEl) tab.trayPillEl.remove();
@@ -1150,6 +1152,86 @@ function clearPlanFinalizationWatchdog(tab) {
1150
1152
  }
1151
1153
  }
1152
1154
 
1155
+ // Watchdog for the kimi-for-coding/k2p6 + OpenCode 1.14.30 deadlock: POST
1156
+ // /question/<id> returns 200 but session.processor never resumes — no second
1157
+ // LLM stream call, no message.part.delta, no session.idle. Without this, the
1158
+ // user sits in "Thinking…" until they manually Stop. After QUESTION_REPLY_WATCHDOG_MS
1159
+ // of total SSE silence post-reply, abort the stuck turn and resend the answer
1160
+ // as a regular `message:send` (which works fine — the bug is specific to the
1161
+ // question-tool resolve path, not normal message turns). Watchdog is cleared
1162
+ // in `handleSSEEvent` on any SSE event for this tab, in `abortTab`, and at the
1163
+ // start of `sendMessage`.
1164
+ const QUESTION_REPLY_WATCHDOG_MS = 75000;
1165
+
1166
+ function buildAnswerSyntheticMessage(questions, answers) {
1167
+ const fmt = (v) => Array.isArray(v) ? v.map(x => String(x ?? '').trim()).filter(Boolean).join(', ') : String(v ?? '').trim();
1168
+ if (typeof answers === 'string') return answers.trim();
1169
+ if (!answers || typeof answers !== 'object') return '';
1170
+ const entries = Object.entries(answers).filter(([, v]) => fmt(v));
1171
+ if (entries.length === 0) return '';
1172
+ if (entries.length === 1) return fmt(entries[0][1]);
1173
+ const lines = entries.map(([q, a]) => `- ${q}: ${fmt(a)}`);
1174
+ return `Here are my answers:\n${lines.join('\n')}`;
1175
+ }
1176
+
1177
+ function scheduleQuestionReplyWatchdog(tab, syntheticMessage) {
1178
+ if (!tab || !syntheticMessage) return;
1179
+ if (tab._questionReplyWatchdogId) clearTimeout(tab._questionReplyWatchdogId);
1180
+ tab._questionReplyAnswerSnapshot = syntheticMessage;
1181
+ tab._questionReplyWatchdogId = setTimeout(() => {
1182
+ const snap = tab._questionReplyAnswerSnapshot;
1183
+ tab._questionReplyWatchdogId = null;
1184
+ tab._questionReplyAnswerSnapshot = null;
1185
+ if (!snap || !tab.running) return;
1186
+ executeQuestionReplyRecovery(tab, snap);
1187
+ }, QUESTION_REPLY_WATCHDOG_MS);
1188
+ }
1189
+
1190
+ function clearQuestionReplyWatchdog(tab) {
1191
+ if (tab?._questionReplyWatchdogId) {
1192
+ clearTimeout(tab._questionReplyWatchdogId);
1193
+ tab._questionReplyWatchdogId = null;
1194
+ }
1195
+ if (tab) tab._questionReplyAnswerSnapshot = null;
1196
+ }
1197
+
1198
+ async function executeQuestionReplyRecovery(tab, syntheticMessage) {
1199
+ // sendMessage operates on activeTab(); if the user switched away while
1200
+ // waiting, switch back so the synthetic recovery lands in the right tab.
1201
+ if (tab !== activeTab()) {
1202
+ const idx = _tabs.indexOf(tab);
1203
+ if (idx >= 0) switchTab(idx);
1204
+ }
1205
+ const live = panelEl('#ocp-messages');
1206
+ if (live) {
1207
+ renderErrorMessage(live, "Model didn't resume after answering. Auto-recovering — resending your answer as a regular message…");
1208
+ removeThinking(live);
1209
+ finalizeStreamingMessage(live);
1210
+ }
1211
+ if (tab._activeSendRequestId) {
1212
+ try { rejectPending(tab._activeSendRequestId, 'Question stall recovery'); } catch {}
1213
+ if (_activeSendRequestId === tab._activeSendRequestId) _activeSendRequestId = null;
1214
+ tab._activeSendRequestId = null;
1215
+ }
1216
+ setActiveQuestionToolId(tab, null);
1217
+ tab.running = false;
1218
+ tab.turnStartedAt = 0;
1219
+ endTurn(tab);
1220
+ setTurnStatus(tab);
1221
+ update();
1222
+ // Await the abort so the stuck message is closed server-side before we
1223
+ // start a fresh turn — prevents a late `session.idle` from the abort
1224
+ // racing into the new turn and flipping `tab.running` back to false.
1225
+ if (tab.sessionId) {
1226
+ try { await requestWs('message:abort', { sessionId: tab.sessionId }, 5000); } catch {}
1227
+ }
1228
+ try {
1229
+ await sendMessage(syntheticMessage);
1230
+ } catch (err) {
1231
+ console.error('[ocp-tabs] question stall recovery sendMessage failed:', err);
1232
+ }
1233
+ }
1234
+
1153
1235
  export function applySavedPlan(tab, content, filePath = '') {
1154
1236
  if (!tab || !content) return;
1155
1237
  tab.planContent = content;
@@ -1195,6 +1277,7 @@ export async function sendMessage(content, options = {}) {
1195
1277
  tab._exitPlanDetected = false;
1196
1278
  tab._pendingPostPlanCheck = false;
1197
1279
  clearPlanFinalizationWatchdog(tab);
1280
+ clearQuestionReplyWatchdog(tab);
1198
1281
 
1199
1282
  const turnStartedAt = Date.now();
1200
1283
  tab.running = true;
@@ -1406,6 +1489,7 @@ function abortTab(tab, container = null) {
1406
1489
  endTurn(tab);
1407
1490
  setTurnStatus(tab);
1408
1491
  clearPlanFinalizationWatchdog(tab);
1492
+ clearQuestionReplyWatchdog(tab);
1409
1493
  return true;
1410
1494
  }
1411
1495
 
@@ -1896,6 +1980,9 @@ export function handleSSEEvent(eventType, event) {
1896
1980
  // distinguished from a truly dead session (sendMessage uses this to suppress
1897
1981
  // the red error toast when SSE is still streaming).
1898
1982
  tab._lastEventAt = Date.now();
1983
+ // Any SSE event after a question reply means the LLM resumed — clear the
1984
+ // post-reply watchdog so it doesn't fire spurious recovery.
1985
+ clearQuestionReplyWatchdog(tab);
1899
1986
 
1900
1987
  const isActiveTab = tab === activeTab();
1901
1988
  const container = isActiveTab ? panelEl('#ocp-messages') : null;
@@ -2486,6 +2573,13 @@ function renderOpencodeQuestion(tab, container, event, requestID) {
2486
2573
  startedAt: Date.now(),
2487
2574
  waiting: true,
2488
2575
  });
2576
+ // Arm the no-resume watchdog. Auto-recovers from the
2577
+ // kimi-for-coding/k2p6 + OpenCode 1.14.30 deadlock where the POST
2578
+ // is accepted but session.processor never resumes the LLM stream.
2579
+ if (!resp?.status || resp.status < 400) {
2580
+ const synthetic = buildAnswerSyntheticMessage(questions, answers);
2581
+ if (synthetic) scheduleQuestionReplyWatchdog(tab, synthetic);
2582
+ }
2489
2583
  processQuestionQueue(tab, container);
2490
2584
  update();
2491
2585
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "synabun",
3
- "version": "2026.5.2-beta",
3
+ "version": "2026.5.3-beta",
4
4
  "description": "Persistent vector memory for AI assistants",
5
5
  "license": "Apache-2.0",
6
6
  "author": "The SynaBun Authors",