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
|
}
|