maka-agent 0.2.0-dev.45.20260921 → 0.2.0-dev.47.20260922
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/DISCLAIMER-WIP +6 -1
- package/README.md +6 -1
- package/dist/acp/maka-acp-agent.js +1 -1
- package/dist/acp/session-mcp.js +212 -0
- package/dist/acp/session-registry.js +52 -8
- package/dist/acp/stdio-server.js +3 -0
- package/dist/cli-core.js +1 -1
- package/dist/mcp-capability-provider.js +6 -1
- package/dist/mcp-capability-publication.js +139 -0
- package/dist/pi-tui-layout.js +46 -17
- package/dist/pi-tui-pickers.js +25 -14
- package/dist/pi-tui-runner.js +24 -15
- package/dist/runtime-host-service-manager.js +26 -2
- package/dist/tui-context-refresh.js +15 -26
- package/dist/tui-mcp-control.js +29 -88
- package/native/runtime-host-windows-task-launcher/prebuilds/win32-x64/maka-runtime-host-task-launcher.exe +0 -0
- package/node_modules/@maka/core/dist/client-capability-grant.js +2 -2
- package/node_modules/@maka/core/dist/model-thinking.js +36 -1
- package/node_modules/@maka/core/dist/provider-registry.js +0 -1
- package/node_modules/@maka/core/dist/refresh-read-coordinator.js +62 -0
- package/node_modules/@maka/core/dist/runtime-policy/connection-catalog-codec.js +4 -0
- package/node_modules/@maka/core/dist/session.js +17 -1
- package/node_modules/@maka/core/dist/settings.js +2 -0
- package/node_modules/@maka/core/package.json +1 -0
- package/node_modules/@maka/runtime/dist/ai-sdk-message-projection.js +15 -60
- package/node_modules/@maka/runtime/dist/ai-sdk-turn.js +18 -0
- package/node_modules/@maka/runtime/dist/apply-patch-profile.js +64 -14
- package/node_modules/@maka/runtime/dist/builtin-tools.js +3 -2
- package/node_modules/@maka/runtime/dist/codex-v4a-patch.js +18 -0
- package/node_modules/@maka/runtime/dist/model-adapter.js +80 -3
- package/node_modules/@maka/runtime/dist/model-runtime.js +6 -0
- package/node_modules/@maka/runtime/dist/openai-apply-patch.js +5 -0
- package/node_modules/@maka/runtime/dist/plugin-executor-backend.js +6 -0
- package/node_modules/@maka/runtime/dist/session-manager.js +15 -2
- package/node_modules/@maka/runtime-host/dist/client/capability-provider-service.js +2 -0
- package/node_modules/@maka/runtime-host/dist/client/client-capability-channel.js +59 -20
- package/node_modules/@maka/runtime-host/dist/client/connection.js +6 -4
- package/node_modules/@maka/runtime-host/dist/client/reconnecting-connection.js +4 -4
- package/node_modules/@maka/runtime-host/dist/protocol/client-capability.js +20 -3
- package/node_modules/@maka/runtime-host/dist/protocol/external-session.js +8 -5
- package/node_modules/@maka/runtime-host/dist/protocol/index.js +11 -1
- package/node_modules/@maka/runtime-host/dist/protocol/session-catalog.js +35 -3
- package/node_modules/@maka/runtime-host/dist/protocol/workhub-coordination.js +1 -5
- package/node_modules/@maka/runtime-host/dist/server/client-capability-coordinator.js +234 -70
- package/node_modules/@maka/runtime-host/dist/server/client-capability-invocation-broker.js +14 -8
- package/node_modules/@maka/runtime-host/dist/server/execution-composition.js +29 -2
- package/node_modules/@maka/runtime-host/dist/server/execution-model-authority.js +3 -0
- package/node_modules/@maka/runtime-host/dist/server/external-session-coordinator.js +29 -7
- package/node_modules/@maka/runtime-host/dist/server/hosted-execution-tool-profile.js +1 -1
- package/node_modules/@maka/runtime-host/dist/server/session-bundle-coordinator.js +4 -1
- package/node_modules/@maka/runtime-host/dist/server/session-catalog-coordinator.js +32 -4
- package/node_modules/@maka/runtime-host/dist/server/session-retirement-coordinator.js +2 -2
- package/package.json +1 -1
package/dist/pi-tui-layout.js
CHANGED
|
@@ -150,6 +150,13 @@ export class MakaPiLayoutComponent extends Container {
|
|
|
150
150
|
previousLines;
|
|
151
151
|
previousRows;
|
|
152
152
|
previousWidth;
|
|
153
|
+
/**
|
|
154
|
+
* An in-turn question is live chrome, not a modal picker: reserving its rows
|
|
155
|
+
* keeps the transcript tail above it instead of compositing the prompt over
|
|
156
|
+
* model output. The runner owns the interaction lifecycle; this component
|
|
157
|
+
* owns the resulting terminal geometry.
|
|
158
|
+
*/
|
|
159
|
+
blockingInteraction;
|
|
153
160
|
constructor(state, transcript, activityStrip, pendingQueue, editor, statusLine, terminal, todoIndicator) {
|
|
154
161
|
super();
|
|
155
162
|
this.state = state;
|
|
@@ -168,45 +175,66 @@ export class MakaPiLayoutComponent extends Container {
|
|
|
168
175
|
this.addChild(editor);
|
|
169
176
|
this.addChild(statusLine);
|
|
170
177
|
}
|
|
178
|
+
setBlockingInteraction(interaction) {
|
|
179
|
+
if (this.blockingInteraction === interaction)
|
|
180
|
+
return;
|
|
181
|
+
if (this.blockingInteraction)
|
|
182
|
+
this.removeChild(this.blockingInteraction);
|
|
183
|
+
this.blockingInteraction = interaction;
|
|
184
|
+
if (interaction)
|
|
185
|
+
this.addChild(interaction);
|
|
186
|
+
}
|
|
171
187
|
render(width) {
|
|
172
188
|
const transcriptLines = this.transcript.render(width);
|
|
173
189
|
const activityLines = this.activityStrip.render(width);
|
|
174
190
|
const allPendingLines = this.pendingQueue.render(width);
|
|
175
191
|
const statusLines = this.statusLine.render(width);
|
|
176
|
-
|
|
192
|
+
const blockingInteraction = this.blockingInteraction;
|
|
193
|
+
// #1064: separate an active strip from the last transcript line. Reserve
|
|
194
|
+
// both that gap and a content row before budgeting a blocking question.
|
|
195
|
+
const activityActive = activityLines.length > 0 && activityLines.some((line) => line.length > 0);
|
|
196
|
+
const lastTranscriptLine = transcriptLines[transcriptLines.length - 1];
|
|
197
|
+
const needGap = activityActive && lastTranscriptLine !== undefined && lastTranscriptLine.length > 0;
|
|
198
|
+
const paddedTranscript = needGap ? [...transcriptLines, ''] : transcriptLines;
|
|
199
|
+
const transcriptRows = blockingInteraction
|
|
200
|
+
? Math.min(paddedTranscript.length, needGap ? 2 : 1)
|
|
201
|
+
: 0;
|
|
202
|
+
const interactionMargin = blockingInteraction ? 1 : 0;
|
|
203
|
+
const input = blockingInteraction ?? this.editor;
|
|
204
|
+
const minimumInputRows = input.minimumViewportRows();
|
|
205
|
+
// Supplementary information yields to the active input and transcript.
|
|
177
206
|
const todoLines = this.terminal.rows >
|
|
178
207
|
activityLines.length +
|
|
179
208
|
allPendingLines.length +
|
|
180
209
|
statusLines.length +
|
|
181
|
-
|
|
210
|
+
transcriptRows +
|
|
211
|
+
interactionMargin +
|
|
212
|
+
minimumInputRows
|
|
182
213
|
? (this.todoIndicator?.render(width) ?? []).slice(0, 1)
|
|
183
214
|
: [];
|
|
184
|
-
const pendingRowsAvailable = this.editor.isShowingAutocomplete()
|
|
215
|
+
const pendingRowsAvailable = blockingInteraction || this.editor.isShowingAutocomplete()
|
|
185
216
|
? Math.max(0, this.terminal.rows -
|
|
186
217
|
activityLines.length -
|
|
187
218
|
statusLines.length -
|
|
188
219
|
todoLines.length -
|
|
189
|
-
|
|
220
|
+
transcriptRows -
|
|
221
|
+
interactionMargin -
|
|
222
|
+
minimumInputRows)
|
|
190
223
|
: allPendingLines.length;
|
|
191
224
|
const pendingLines = fitPendingQueueLines(allPendingLines, pendingRowsAvailable);
|
|
192
|
-
|
|
225
|
+
input.setViewportRows(this.terminal.rows -
|
|
193
226
|
activityLines.length -
|
|
194
227
|
pendingLines.length -
|
|
195
228
|
statusLines.length -
|
|
196
|
-
todoLines.length
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
// thinking or tool row (the agent-work stack, which has no internal blank
|
|
201
|
-
// gaps) sits directly against `Working… 12s`.
|
|
202
|
-
const activityActive = activityLines.length > 0 && activityLines.some((line) => line.length > 0);
|
|
203
|
-
const lastTranscriptLine = transcriptLines[transcriptLines.length - 1];
|
|
204
|
-
const needGap = activityActive && lastTranscriptLine !== undefined && lastTranscriptLine.length > 0;
|
|
205
|
-
const paddedTranscript = needGap ? [...transcriptLines, ''] : transcriptLines;
|
|
229
|
+
todoLines.length -
|
|
230
|
+
transcriptRows -
|
|
231
|
+
interactionMargin);
|
|
232
|
+
const inputLines = input.render(width);
|
|
206
233
|
const chromeRows = activityLines.length +
|
|
207
234
|
pendingLines.length +
|
|
208
235
|
todoLines.length +
|
|
209
|
-
|
|
236
|
+
inputLines.length +
|
|
237
|
+
interactionMargin +
|
|
210
238
|
statusLines.length;
|
|
211
239
|
const viewportRows = Math.max(0, this.terminal.rows - chromeRows);
|
|
212
240
|
const paddingRows = Math.max(0, viewportRows - paddedTranscript.length);
|
|
@@ -216,7 +244,8 @@ export class MakaPiLayoutComponent extends Container {
|
|
|
216
244
|
...activityLines,
|
|
217
245
|
...pendingLines,
|
|
218
246
|
...todoLines,
|
|
219
|
-
...
|
|
247
|
+
...inputLines,
|
|
248
|
+
...(blockingInteraction ? [''] : []),
|
|
220
249
|
...statusLines,
|
|
221
250
|
];
|
|
222
251
|
// #1097: record where pi-tui's live viewport starts for this render, in
|
package/dist/pi-tui-pickers.js
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* specific language governing permissions and limitations
|
|
17
17
|
* under the License.
|
|
18
18
|
*/
|
|
19
|
-
import { CombinedAutocompleteProvider, Editor, Key, SelectList, decodeKittyPrintable, isKeyRelease, isKeyRepeat, matchesKey, truncateToWidth, visibleWidth, wrapTextWithAnsi, } from '@earendil-works/pi-tui';
|
|
19
|
+
import { CombinedAutocompleteProvider, CURSOR_MARKER, Editor, Key, SelectList, decodeKittyPrintable, isKeyRelease, isKeyRepeat, matchesKey, truncateToWidth, visibleWidth, wrapTextWithAnsi, } from '@earendil-works/pi-tui';
|
|
20
20
|
import { defineUiMessageCatalog, formatUiMessage, resolveUiMessageCatalog, } from '@maka/core/ui-locale';
|
|
21
21
|
import { providerDefaultsOf, validateSlug, } from '@maka/core/llm-connections';
|
|
22
22
|
import { CONNECTION_NAME_MAX_LENGTH } from '@maka/core/runtime-policy';
|
|
@@ -333,6 +333,7 @@ const USER_QUESTION_ROW_PREFIX_WIDTH = 2;
|
|
|
333
333
|
export class UserQuestionOverlay {
|
|
334
334
|
input;
|
|
335
335
|
editor;
|
|
336
|
+
viewportRows = Number.POSITIVE_INFINITY;
|
|
336
337
|
// Highlight index over [0, options.length]. `options.length` is the input row.
|
|
337
338
|
activeIndex = 0;
|
|
338
339
|
constructor(tui, input) {
|
|
@@ -352,6 +353,13 @@ export class UserQuestionOverlay {
|
|
|
352
353
|
get inputRowIndex() {
|
|
353
354
|
return this.input.options.length;
|
|
354
355
|
}
|
|
356
|
+
minimumViewportRows() {
|
|
357
|
+
// One title, every option, the answer field, and the closing divider.
|
|
358
|
+
return this.input.options.length + 3;
|
|
359
|
+
}
|
|
360
|
+
setViewportRows(rows) {
|
|
361
|
+
this.viewportRows = Math.max(this.minimumViewportRows(), Math.floor(rows));
|
|
362
|
+
}
|
|
355
363
|
get onInputRow() {
|
|
356
364
|
return this.activeIndex === this.inputRowIndex;
|
|
357
365
|
}
|
|
@@ -411,30 +419,33 @@ export class UserQuestionOverlay {
|
|
|
411
419
|
const hint = padLine(ansi.dim(this.input.hint), safeWidth);
|
|
412
420
|
const blank = padLine('', safeWidth);
|
|
413
421
|
const divider = padLine(ansi.accent('-'.repeat(safeWidth)), safeWidth);
|
|
414
|
-
const
|
|
422
|
+
const allInputRows = this.renderInputRow(safeWidth);
|
|
423
|
+
const inputBudget = Math.max(1, this.viewportRows - this.input.options.length - 2);
|
|
424
|
+
const cursorRow = Math.max(0, allInputRows.findIndex((line) => line.includes(CURSOR_MARKER)));
|
|
425
|
+
const inputStart = Math.max(0, Math.min(cursorRow, allInputRows.length - inputBudget));
|
|
426
|
+
const inputRows = allInputRows.slice(inputStart, inputStart + inputBudget);
|
|
415
427
|
const optionRows = this.input.options.map((option, index) => formatUserQuestionOptionRow(option, index === this.activeIndex, safeWidth));
|
|
416
|
-
const assemble = (title, options) => [
|
|
428
|
+
const assemble = (title, options, help = [hint, blank]) => [
|
|
417
429
|
...title,
|
|
418
|
-
|
|
419
|
-
blank,
|
|
430
|
+
...help,
|
|
420
431
|
...options.flat(),
|
|
421
432
|
...inputRows,
|
|
422
433
|
divider,
|
|
423
434
|
];
|
|
424
435
|
const full = assemble(titleLines, optionRows);
|
|
425
|
-
const budget = this.
|
|
436
|
+
const budget = this.viewportRows;
|
|
426
437
|
if (full.length <= budget)
|
|
427
438
|
return full;
|
|
428
|
-
//
|
|
429
|
-
//
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
const
|
|
434
|
-
const fixedRows = cappedTitle.length +
|
|
439
|
+
// The layout owns the available height. Short screens first lose spacing
|
|
440
|
+
// and the hint, retaining the question, every choice, and the answer field.
|
|
441
|
+
const titleBudget = Math.min(2, Math.max(1, budget - this.input.options.length - inputRows.length - 3));
|
|
442
|
+
const cappedTitle = clampRowsWithEllipsis(titleLines, titleBudget, safeWidth);
|
|
443
|
+
const helpBudget = Math.max(0, budget - cappedTitle.length - this.input.options.length - inputRows.length - 1);
|
|
444
|
+
const help = [hint, blank].slice(0, helpBudget);
|
|
445
|
+
const fixedRows = cappedTitle.length + help.length + inputRows.length + 1;
|
|
435
446
|
const optionBudget = Math.max(this.input.options.length, budget - fixedRows);
|
|
436
447
|
const perOption = Math.max(1, Math.floor(optionBudget / Math.max(1, this.input.options.length)));
|
|
437
|
-
return assemble(cappedTitle, optionRows.map((rows) => clampRowsWithEllipsis(rows, perOption, safeWidth)));
|
|
448
|
+
return assemble(cappedTitle, optionRows.map((rows) => clampRowsWithEllipsis(rows, perOption, safeWidth)), help);
|
|
438
449
|
}
|
|
439
450
|
renderInputRow(width) {
|
|
440
451
|
const marker = this.onInputRow ? '→' : ' ';
|
package/dist/pi-tui-runner.js
CHANGED
|
@@ -240,7 +240,7 @@ export async function runMakaPiTui(input) {
|
|
|
240
240
|
};
|
|
241
241
|
};
|
|
242
242
|
let userQuestionInFlight = false;
|
|
243
|
-
let
|
|
243
|
+
let userQuestionPrompt;
|
|
244
244
|
let userQuestionProgress;
|
|
245
245
|
let formResponseInFlightRequestId;
|
|
246
246
|
let formOverlay;
|
|
@@ -1783,9 +1783,14 @@ export async function runMakaPiTui(input) {
|
|
|
1783
1783
|
maxHeight: '100%',
|
|
1784
1784
|
margin: { bottom: BOTTOM_PICKER_MARGIN_ROWS },
|
|
1785
1785
|
});
|
|
1786
|
-
const
|
|
1787
|
-
|
|
1788
|
-
|
|
1786
|
+
const closeUserQuestionPrompt = () => {
|
|
1787
|
+
const prompt = userQuestionPrompt;
|
|
1788
|
+
if (!prompt)
|
|
1789
|
+
return;
|
|
1790
|
+
layout.setBlockingInteraction(undefined);
|
|
1791
|
+
userQuestionPrompt = undefined;
|
|
1792
|
+
if (tui.getFocusedComponent() === prompt)
|
|
1793
|
+
tui.setFocus(editorSurface);
|
|
1789
1794
|
};
|
|
1790
1795
|
const finishUserQuestion = (requestId, answers) => {
|
|
1791
1796
|
if (userQuestionInFlight)
|
|
@@ -1796,7 +1801,7 @@ export async function runMakaPiTui(input) {
|
|
|
1796
1801
|
return;
|
|
1797
1802
|
}
|
|
1798
1803
|
userQuestionInFlight = true;
|
|
1799
|
-
|
|
1804
|
+
closeUserQuestionPrompt();
|
|
1800
1805
|
void respond
|
|
1801
1806
|
.call(input.driver, { requestId, answers })
|
|
1802
1807
|
.then(() => {
|
|
@@ -1824,36 +1829,35 @@ export async function runMakaPiTui(input) {
|
|
|
1824
1829
|
finishUserQuestion(request.requestId, progress.answers);
|
|
1825
1830
|
return;
|
|
1826
1831
|
}
|
|
1827
|
-
|
|
1832
|
+
closeUserQuestionPrompt();
|
|
1828
1833
|
const advance = (answer) => {
|
|
1829
1834
|
progress.answers[progress.index] = answer;
|
|
1830
1835
|
progress.index += 1;
|
|
1831
1836
|
showUserQuestion();
|
|
1832
1837
|
};
|
|
1833
|
-
|
|
1838
|
+
userQuestionPrompt = new UserQuestionOverlay(tui, {
|
|
1834
1839
|
title: question.question,
|
|
1835
1840
|
rightLabel: `${progress.index + 1} / ${request.questions.length}`,
|
|
1836
1841
|
hint: '↑↓ move · type to answer · Enter select · Esc unanswered · Ctrl+C stop',
|
|
1837
1842
|
placeholder: 'Other: type your answer…',
|
|
1838
1843
|
options: question.options,
|
|
1839
|
-
// Live budget: terminal.rows changes on resize, so read it per render
|
|
1840
|
-
// rather than at overlay construction.
|
|
1841
|
-
maxRows: () => Math.max(1, terminal.rows - BOTTOM_PICKER_MARGIN_ROWS),
|
|
1842
1844
|
onSelectOption: (index) => advance(question.options[index]?.label ?? null),
|
|
1843
1845
|
onSubmitText: (value) => advance(value),
|
|
1844
1846
|
onSkip: () => advance(null),
|
|
1845
|
-
})
|
|
1847
|
+
});
|
|
1848
|
+
layout.setBlockingInteraction(userQuestionPrompt);
|
|
1849
|
+
tui.setFocus(userQuestionPrompt);
|
|
1846
1850
|
};
|
|
1847
|
-
const
|
|
1851
|
+
const syncUserQuestionPrompt = () => {
|
|
1848
1852
|
const request = activeUserQuestionRequest(state);
|
|
1849
1853
|
if (!request) {
|
|
1850
|
-
|
|
1854
|
+
closeUserQuestionPrompt();
|
|
1851
1855
|
userQuestionProgress = undefined;
|
|
1852
1856
|
return;
|
|
1853
1857
|
}
|
|
1854
1858
|
if (userQuestionInFlight)
|
|
1855
1859
|
return;
|
|
1856
|
-
if (userQuestionProgress?.requestId !== request.requestId) {
|
|
1860
|
+
if (userQuestionProgress?.requestId !== request.requestId || !userQuestionPrompt) {
|
|
1857
1861
|
userQuestionProgress = {
|
|
1858
1862
|
requestId: request.requestId,
|
|
1859
1863
|
index: 0,
|
|
@@ -1978,7 +1982,7 @@ export async function runMakaPiTui(input) {
|
|
|
1978
1982
|
transcriptOverlay?.hide();
|
|
1979
1983
|
transcriptOverlay = undefined;
|
|
1980
1984
|
}
|
|
1981
|
-
|
|
1985
|
+
syncUserQuestionPrompt();
|
|
1982
1986
|
syncFormOverlay();
|
|
1983
1987
|
};
|
|
1984
1988
|
const showSelectPicker = (title, rightLabel, items, onSelect, options) => {
|
|
@@ -4090,6 +4094,11 @@ export async function runMakaPiTui(input) {
|
|
|
4090
4094
|
requestTurnInterrupt();
|
|
4091
4095
|
return { consume: true };
|
|
4092
4096
|
}
|
|
4097
|
+
// AskUserQuestion is rendered inside the live layout so the transcript is
|
|
4098
|
+
// not covered by a modal overlay. Keep the same input-capture boundary as
|
|
4099
|
+
// the old overlay while the prompt owns focus.
|
|
4100
|
+
if (userQuestionPrompt)
|
|
4101
|
+
return undefined;
|
|
4093
4102
|
if (tui.hasOverlay())
|
|
4094
4103
|
return undefined;
|
|
4095
4104
|
if (sideConversation && matchesSideConversationToggle(data)) {
|
|
@@ -24,9 +24,10 @@ import { mkdir, open, readFile, realpath, rename, rm, stat, unlink, } from 'node
|
|
|
24
24
|
import { homedir } from 'node:os';
|
|
25
25
|
import { dirname, isAbsolute, join, resolve } from 'node:path';
|
|
26
26
|
import { truncateUtf8 } from '@maka/core/diagnostic-log';
|
|
27
|
+
import { redactSecrets } from '@maka/core/redaction';
|
|
27
28
|
import { canonicalProjectDirectoryRootSpec, isCanonicalRuntimeHostWebSocketPath, PROJECT_DIRECTORY_MAX_ROOTS, projectDirectoryRootSpecValid, RUNTIME_HOST_PROTOCOL_VERSION, } from '@maka/runtime-host/protocol';
|
|
28
29
|
import { connectExistingRuntimeHost, prepareConnectedRuntimeHostRetirement, } from '@maka/runtime-host/client';
|
|
29
|
-
import { resolveRuntimeHostManagedServiceId, RUNTIME_HOST_SERVICE_LOG_MAX_BYTES, } from '@maka/runtime-host/operator';
|
|
30
|
+
import { resolveRuntimeHostManagedServiceId, RUNTIME_HOST_SERVICE_LOG_MAX_BYTES, RUNTIME_HOST_SERVICE_ERROR_MESSAGE_MAX_BYTES, } from '@maka/runtime-host/operator';
|
|
30
31
|
import { withProcessLifetimeFileUpdateLock } from '@maka/storage/process-lifetime-file-update-lock';
|
|
31
32
|
import { discoverMarkedStorageRoot, resolveExistingStorageRoot, tryAcquireInteractiveRootOwner, } from '@maka/storage/root-authority';
|
|
32
33
|
import { isRuntimeHostManagedDeploymentCli, removeRuntimeHostManagedDeployment, resolveExistingRuntimeHostManagedDeploymentRoot, resolveRuntimeHostManagedDeploymentForCli, resolveRuntimeHostManagedDeploymentRoot, } from './runtime-host-managed-deployment.js';
|
|
@@ -39,6 +40,7 @@ const DEFAULT_WEBSOCKET_PATH = '/runtime-host';
|
|
|
39
40
|
const SERVICE_OPERATION_LOCK_TIMEOUT_MS = 60_000;
|
|
40
41
|
const SERVICE_READY_TIMEOUT_MS = 45_000;
|
|
41
42
|
const SERVICE_READY_POLL_MS = 50;
|
|
43
|
+
const SERVICE_READINESS_LOG_MAX_BYTES = 1_024;
|
|
42
44
|
export class RuntimeHostServiceManagerError extends Error {
|
|
43
45
|
code;
|
|
44
46
|
constructor(code, message, options) {
|
|
@@ -451,6 +453,17 @@ export function formatRuntimeHostServiceLogs(sources) {
|
|
|
451
453
|
})
|
|
452
454
|
.join('\n');
|
|
453
455
|
}
|
|
456
|
+
export function formatRuntimeHostServiceReadinessDiagnostic(input) {
|
|
457
|
+
const lines = [`Runtime Host service did not become ready: ${redactSecrets(input.lastFailure)}`];
|
|
458
|
+
if (input.status) {
|
|
459
|
+
lines.push(`service state: ${input.status.state}; active: ${String(input.status.active)}; pid: ${input.status.pid ?? 'none'}; last exit code: ${input.status.lastExitCode ?? 'unknown'}`);
|
|
460
|
+
}
|
|
461
|
+
if (input.logs?.trim()) {
|
|
462
|
+
const safeLogs = takeUtf8Tail(redactSecrets(input.logs.trim()), SERVICE_READINESS_LOG_MAX_BYTES);
|
|
463
|
+
lines.push(`service logs (tail):\n${safeLogs}`);
|
|
464
|
+
}
|
|
465
|
+
return truncateUtf8(lines.join('\n'), RUNTIME_HOST_SERVICE_ERROR_MESSAGE_MAX_BYTES, '\n<diagnostic truncated>');
|
|
466
|
+
}
|
|
454
467
|
function takeUtf8Tail(value, maximumBytes) {
|
|
455
468
|
const encoded = Buffer.from(value);
|
|
456
469
|
if (encoded.byteLength <= maximumBytes)
|
|
@@ -738,7 +751,18 @@ export async function verifyRuntimeHostManagedServiceReady(config, backend) {
|
|
|
738
751
|
}
|
|
739
752
|
await new Promise((resolveWait) => setTimeout(resolveWait, SERVICE_READY_POLL_MS));
|
|
740
753
|
}
|
|
741
|
-
throw new RuntimeHostServiceManagerError('service_manager_operation_failed',
|
|
754
|
+
throw new RuntimeHostServiceManagerError('service_manager_operation_failed', await formatRuntimeHostServiceReadinessFailure(lastFailure, backend));
|
|
755
|
+
}
|
|
756
|
+
async function formatRuntimeHostServiceReadinessFailure(lastFailure, backend) {
|
|
757
|
+
const [status, logs] = await Promise.all([
|
|
758
|
+
backend.status().catch(() => undefined),
|
|
759
|
+
backend.logs().catch(() => undefined),
|
|
760
|
+
]);
|
|
761
|
+
return formatRuntimeHostServiceReadinessDiagnostic({
|
|
762
|
+
lastFailure,
|
|
763
|
+
status,
|
|
764
|
+
logs,
|
|
765
|
+
});
|
|
742
766
|
}
|
|
743
767
|
async function prepareRuntimeHostRetirement(config, expectedPid, allowInterruptActiveTasks) {
|
|
744
768
|
const connected = await connectExistingRuntimeHost({
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
* specific language governing permissions and limitations
|
|
17
17
|
* under the License.
|
|
18
18
|
*/
|
|
19
|
+
import { createRefreshReadCoordinator } from '@maka/core/refresh-read-coordinator';
|
|
19
20
|
/**
|
|
20
21
|
* How full the context is right now, re-asked mid-turn (#4545).
|
|
21
22
|
*
|
|
@@ -55,44 +56,32 @@ export function isCtxRefreshRelevantEvent(event) {
|
|
|
55
56
|
export const CTX_REFRESH_DEBOUNCE_MS = 400;
|
|
56
57
|
/**
|
|
57
58
|
* Coalesces a burst of refresh-worthy events into one query, and lets only
|
|
58
|
-
* the latest
|
|
59
|
-
* issued
|
|
60
|
-
*
|
|
61
|
-
* torn-down session is not owed an update
|
|
62
|
-
* last value standing: it is still the
|
|
59
|
+
* the latest valid query apply. A query that resolves after a newer one was
|
|
60
|
+
* issued, after a relevant event invalidated it, or after `cancel` retired it
|
|
61
|
+
* is dropped: the answer a slow read brings back describes an older snapshot
|
|
62
|
+
* than the one already shown, and a torn-down session is not owed an update
|
|
63
|
+
* at all. A failed query leaves the last value standing: it is still the
|
|
64
|
+
* newest answer anyone has.
|
|
63
65
|
*
|
|
64
66
|
* The scheduler is injected so the policy is testable without a wall clock,
|
|
65
67
|
* and follows the CLI's ticker convention: schedule returns the cancel.
|
|
66
68
|
*/
|
|
67
69
|
export function createCtxRefresher(input) {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
};
|
|
74
|
-
const run = () => {
|
|
75
|
-
cancelScheduled = undefined;
|
|
76
|
-
const requestRevision = ++revision;
|
|
77
|
-
void input.query().then((result) => {
|
|
78
|
-
if (requestRevision !== revision)
|
|
79
|
-
return;
|
|
80
|
-
input.apply(result);
|
|
81
|
-
}, () => { });
|
|
82
|
-
};
|
|
70
|
+
const coordinator = createRefreshReadCoordinator({
|
|
71
|
+
read: input.query,
|
|
72
|
+
apply: input.apply,
|
|
73
|
+
delayMs: input.delayMs,
|
|
74
|
+
schedule: input.schedule,
|
|
75
|
+
});
|
|
83
76
|
return {
|
|
84
77
|
observe(event) {
|
|
85
78
|
if (!isCtxRefreshRelevantEvent(event))
|
|
86
79
|
return;
|
|
87
80
|
// Restart rather than stack: the last event of a burst is the one whose
|
|
88
81
|
// snapshot the reader wants, and an earlier timer would read before it.
|
|
89
|
-
|
|
90
|
-
cancelScheduled = input.schedule(run, input.delayMs);
|
|
91
|
-
},
|
|
92
|
-
cancel() {
|
|
93
|
-
dropScheduled();
|
|
94
|
-
revision += 1;
|
|
82
|
+
coordinator.observe();
|
|
95
83
|
},
|
|
84
|
+
cancel: coordinator.cancel,
|
|
96
85
|
};
|
|
97
86
|
}
|
|
98
87
|
/** Default one-shot timer; unref'd so a pending refresh never holds the CLI open. */
|
package/dist/tui-mcp-control.js
CHANGED
|
@@ -22,6 +22,7 @@ import { createCredentialMcpOAuthStorage, McpClientManager } from '@maka/mcp';
|
|
|
22
22
|
import { createFileCredentialStore } from '@maka/storage/credential-store';
|
|
23
23
|
import { AtomicFileWriteCommitUnknownError, createMcpConfigStore, assertMcpEndpointPolicyOnChanges, McpConfigSourceError, normalizeMcpConfig, normalizeMcpImport, } from '@maka/storage/mcp-config-store';
|
|
24
24
|
import { createMcpCapabilityProvider } from './mcp-capability-provider.js';
|
|
25
|
+
import { McpCapabilityPublication } from './mcp-capability-publication.js';
|
|
25
26
|
const RUNTIME_HOST_CREDENTIAL_ENV = 'MAKA_RUNTIME_HOST_ACCESS_CREDENTIAL';
|
|
26
27
|
export function createTuiMcpController(input, overrides = {}) {
|
|
27
28
|
const manager = overrides.manager ??
|
|
@@ -49,9 +50,7 @@ class TuiMcpControllerImpl {
|
|
|
49
50
|
#preparedImport;
|
|
50
51
|
#actionLane = Promise.resolve();
|
|
51
52
|
#publicationSuppressed = false;
|
|
52
|
-
#
|
|
53
|
-
#publicationTask;
|
|
54
|
-
#published;
|
|
53
|
+
#publication;
|
|
55
54
|
#snapshot = freezeSnapshot({
|
|
56
55
|
initialization: 'loading',
|
|
57
56
|
configuration: 'synchronizing',
|
|
@@ -67,6 +66,24 @@ class TuiMcpControllerImpl {
|
|
|
67
66
|
...this.#snapshot,
|
|
68
67
|
canManagePublicationCredential: Boolean(connection.setCredential && connection.removeCredential),
|
|
69
68
|
});
|
|
69
|
+
this.#publication = new McpCapabilityPublication({
|
|
70
|
+
connectionIdentity: () => this.#availability.kind === 'connected'
|
|
71
|
+
? connectionIdentity(this.#availability)
|
|
72
|
+
: undefined,
|
|
73
|
+
revision: () => this.#deps.manager.toolSnapshot().revision,
|
|
74
|
+
createProvider: () => this.#deps.createProvider(this.#deps.manager),
|
|
75
|
+
replace: (provider) => this.#connection.replaceClientCapabilities(provider),
|
|
76
|
+
unregister: () => this.#connection.unregisterClientCapabilities(),
|
|
77
|
+
onState: (state) => {
|
|
78
|
+
this.#updateSnapshot({
|
|
79
|
+
publication: state === 'unavailable'
|
|
80
|
+
? this.#availability.kind === 'unavailable'
|
|
81
|
+
? (this.#availability.reason ?? 'host_unavailable')
|
|
82
|
+
: 'waiting'
|
|
83
|
+
: state,
|
|
84
|
+
});
|
|
85
|
+
},
|
|
86
|
+
});
|
|
70
87
|
this.#disposeManagerChange = deps.manager.onChange(() => {
|
|
71
88
|
try {
|
|
72
89
|
this.#refreshManagerSnapshot();
|
|
@@ -81,7 +98,7 @@ class TuiMcpControllerImpl {
|
|
|
81
98
|
this.#disposeConnectionAvailability = connection.subscribeConnectionAvailability((availability) => {
|
|
82
99
|
this.#availability = availability;
|
|
83
100
|
if (availability.kind === 'unavailable') {
|
|
84
|
-
this.#
|
|
101
|
+
this.#publication.invalidate();
|
|
85
102
|
this.#updateSnapshot({
|
|
86
103
|
publication: availability.reason ?? 'host_unavailable',
|
|
87
104
|
...(availability.reason === 'provider_conflict'
|
|
@@ -161,15 +178,11 @@ class TuiMcpControllerImpl {
|
|
|
161
178
|
this.#disposeConnectionAvailability();
|
|
162
179
|
this.#listeners.clear();
|
|
163
180
|
this.#preparedImport = undefined;
|
|
164
|
-
this.#
|
|
181
|
+
const publicationClosing = this.#publication.close().catch(() => undefined);
|
|
165
182
|
const managerClosing = this.#deps.manager.close();
|
|
166
183
|
await this.#actionLane.catch(() => undefined);
|
|
167
184
|
this.#config = undefined;
|
|
168
|
-
await
|
|
169
|
-
if (this.#availability.kind === 'connected') {
|
|
170
|
-
await this.#connection.unregisterClientCapabilities().catch(() => undefined);
|
|
171
|
-
}
|
|
172
|
-
this.#published = undefined;
|
|
185
|
+
await publicationClosing;
|
|
173
186
|
await this.#connection.closePublication?.().catch(() => undefined);
|
|
174
187
|
await managerClosing;
|
|
175
188
|
await this.#initialization.catch(() => undefined);
|
|
@@ -400,9 +413,10 @@ class TuiMcpControllerImpl {
|
|
|
400
413
|
}
|
|
401
414
|
}
|
|
402
415
|
async #settlePublication() {
|
|
403
|
-
this.#
|
|
404
|
-
|
|
405
|
-
|
|
416
|
+
if (!this.#closed &&
|
|
417
|
+
this.#snapshot.initialization === 'ready' &&
|
|
418
|
+
!this.#publicationSuppressed) {
|
|
419
|
+
await this.#publication.settle();
|
|
406
420
|
}
|
|
407
421
|
if (this.#snapshot.publication === 'error' ||
|
|
408
422
|
this.#snapshot.publication === 'credential_rejected' ||
|
|
@@ -436,74 +450,9 @@ class TuiMcpControllerImpl {
|
|
|
436
450
|
this.#notify();
|
|
437
451
|
}
|
|
438
452
|
#requestPublication() {
|
|
439
|
-
if (this.#closed)
|
|
440
|
-
this.#publicationRequested = false;
|
|
453
|
+
if (this.#closed || this.#snapshot.initialization !== 'ready' || this.#publicationSuppressed)
|
|
441
454
|
return;
|
|
442
|
-
|
|
443
|
-
if (this.#snapshot.initialization !== 'ready' || this.#publicationSuppressed)
|
|
444
|
-
return;
|
|
445
|
-
this.#publicationRequested = true;
|
|
446
|
-
if (this.#publicationTask)
|
|
447
|
-
return;
|
|
448
|
-
this.#publicationTask = this.#runPublicationQueue().finally(() => {
|
|
449
|
-
this.#publicationTask = undefined;
|
|
450
|
-
if (this.#publicationRequested)
|
|
451
|
-
this.#requestPublication();
|
|
452
|
-
});
|
|
453
|
-
}
|
|
454
|
-
async #runPublicationQueue() {
|
|
455
|
-
while (this.#publicationRequested && !this.#closed) {
|
|
456
|
-
this.#publicationRequested = false;
|
|
457
|
-
await this.#publishCurrentSnapshot();
|
|
458
|
-
}
|
|
459
|
-
}
|
|
460
|
-
async #publishCurrentSnapshot() {
|
|
461
|
-
const availability = this.#availability;
|
|
462
|
-
if (availability.kind !== 'connected') {
|
|
463
|
-
this.#updateSnapshot({ publication: availability.reason ?? 'host_unavailable' });
|
|
464
|
-
return;
|
|
465
|
-
}
|
|
466
|
-
const identity = connectionIdentity(availability);
|
|
467
|
-
const revision = this.#deps.manager.toolSnapshot().revision;
|
|
468
|
-
if (this.#published?.identity === identity && this.#published.revision === revision) {
|
|
469
|
-
this.#updateSnapshot({
|
|
470
|
-
publication: this.#snapshot.toolCount === 0 ? 'not_published' : 'published',
|
|
471
|
-
});
|
|
472
|
-
return;
|
|
473
|
-
}
|
|
474
|
-
let provider;
|
|
475
|
-
this.#updateSnapshot({ publication: 'publishing' });
|
|
476
|
-
try {
|
|
477
|
-
provider = this.#deps.createProvider(this.#deps.manager);
|
|
478
|
-
if (provider) {
|
|
479
|
-
await this.#connection.replaceClientCapabilities(provider);
|
|
480
|
-
}
|
|
481
|
-
else if (this.#published?.identity === identity && this.#published.registered) {
|
|
482
|
-
await this.#connection.unregisterClientCapabilities();
|
|
483
|
-
}
|
|
484
|
-
}
|
|
485
|
-
catch {
|
|
486
|
-
await closeProvider(provider);
|
|
487
|
-
if (this.#isCurrent(identity, revision)) {
|
|
488
|
-
this.#updateSnapshot({ publication: 'error' });
|
|
489
|
-
}
|
|
490
|
-
else {
|
|
491
|
-
this.#requestPublication();
|
|
492
|
-
}
|
|
493
|
-
return;
|
|
494
|
-
}
|
|
495
|
-
if (!this.#isCurrent(identity, revision)) {
|
|
496
|
-
this.#requestPublication();
|
|
497
|
-
return;
|
|
498
|
-
}
|
|
499
|
-
this.#published = { identity, revision, registered: provider !== undefined };
|
|
500
|
-
this.#updateSnapshot({ publication: provider ? 'published' : 'not_published' });
|
|
501
|
-
}
|
|
502
|
-
#isCurrent(identity, revision) {
|
|
503
|
-
return (!this.#closed &&
|
|
504
|
-
this.#availability.kind === 'connected' &&
|
|
505
|
-
connectionIdentity(this.#availability) === identity &&
|
|
506
|
-
this.#deps.manager.toolSnapshot().revision === revision);
|
|
455
|
+
this.#publication.request();
|
|
507
456
|
}
|
|
508
457
|
#updateSnapshot(update) {
|
|
509
458
|
this.#snapshot = freezeSnapshot({ ...this.#snapshot, ...update });
|
|
@@ -548,14 +497,6 @@ function freezeSnapshot(snapshot) {
|
|
|
548
497
|
function connectionIdentity(availability) {
|
|
549
498
|
return `${availability.hostEpoch}\0${availability.connectionId}`;
|
|
550
499
|
}
|
|
551
|
-
async function closeProvider(provider) {
|
|
552
|
-
try {
|
|
553
|
-
await provider?.close?.();
|
|
554
|
-
}
|
|
555
|
-
catch {
|
|
556
|
-
// A rejected provider never crossed into Host ownership.
|
|
557
|
-
}
|
|
558
|
-
}
|
|
559
500
|
function cloneConfig(config) {
|
|
560
501
|
return structuredClone(config);
|
|
561
502
|
}
|
|
Binary file
|
|
@@ -41,11 +41,11 @@ export function decodeClientCapabilitySessionGrantKey(value) {
|
|
|
41
41
|
}
|
|
42
42
|
export function decodeClientCapabilityGrantTarget(value) {
|
|
43
43
|
const record = plainRecord(value, 'Client Capability Grant target');
|
|
44
|
-
const capability = oneOf(record.capability, ['browser', 'computer_use', 'desktop_mcp'], 'capability');
|
|
44
|
+
const capability = oneOf(record.capability, ['browser', 'computer_use', 'desktop_mcp', 'mcp'], 'capability');
|
|
45
45
|
const scope = decodeClientCapabilityGrantScope(record.scope);
|
|
46
46
|
if ((capability === 'browser' && scope.kind !== 'browser_origin') ||
|
|
47
47
|
(capability === 'computer_use' && scope.kind !== 'capability') ||
|
|
48
|
-
(capability === 'desktop_mcp' && scope.kind !== 'mcp_tool')) {
|
|
48
|
+
((capability === 'desktop_mcp' || capability === 'mcp') && scope.kind !== 'mcp_tool')) {
|
|
49
49
|
throw new Error('Client Capability Session Grant scope does not match capability');
|
|
50
50
|
}
|
|
51
51
|
return deepFreeze({
|
|
@@ -65,6 +65,39 @@ function deriveThinkingChoices(options) {
|
|
|
65
65
|
}
|
|
66
66
|
return THINKING_LEVELS.filter((level) => choices.has(level));
|
|
67
67
|
}
|
|
68
|
+
/** Known patch-capable models. Unknown models stay off until explicitly enabled. */
|
|
69
|
+
const APPLY_PATCH_MODELS = new Set([
|
|
70
|
+
'gpt-5-codex',
|
|
71
|
+
'gpt-5.1',
|
|
72
|
+
'gpt-5.1-codex',
|
|
73
|
+
'gpt-5.1-codex-mini',
|
|
74
|
+
'gpt-5.1-codex-max',
|
|
75
|
+
'gpt-5.2',
|
|
76
|
+
'gpt-5.2-codex',
|
|
77
|
+
'gpt-5.3-codex',
|
|
78
|
+
'gpt-5.3-codex-spark',
|
|
79
|
+
'gpt-5.4',
|
|
80
|
+
'gpt-5.4-mini',
|
|
81
|
+
'gpt-5.4-nano',
|
|
82
|
+
'gpt-5.4-pro',
|
|
83
|
+
'gpt-5.5',
|
|
84
|
+
'gpt-5.6',
|
|
85
|
+
'gpt-5.6-sol',
|
|
86
|
+
'gpt-5.6-terra',
|
|
87
|
+
'gpt-5.6-luna',
|
|
88
|
+
'gpt-6-astra',
|
|
89
|
+
'deepseek-v4-flash',
|
|
90
|
+
'deepseek-v4-flash-vision-exp',
|
|
91
|
+
'deepseek-v4-pro',
|
|
92
|
+
]);
|
|
93
|
+
/** Shared by model settings and tool routing so the displayed switch matches execution. */
|
|
94
|
+
export function modelApplyPatchEnabled(modelId, override) {
|
|
95
|
+
return (override?.applyPatch ??
|
|
96
|
+
APPLY_PATCH_MODELS.has(modelId
|
|
97
|
+
.trim()
|
|
98
|
+
.toLowerCase()
|
|
99
|
+
.replace(/-\d{4}-\d{2}-\d{2}$/, '')));
|
|
100
|
+
}
|
|
68
101
|
function isRecord(value) {
|
|
69
102
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
70
103
|
}
|
|
@@ -90,6 +123,8 @@ function normalizeModelOverride(entry) {
|
|
|
90
123
|
}
|
|
91
124
|
if (typeof entry.vision === 'boolean')
|
|
92
125
|
declared.vision = entry.vision;
|
|
126
|
+
if (typeof entry.applyPatch === 'boolean')
|
|
127
|
+
declared.applyPatch = entry.applyPatch;
|
|
93
128
|
for (const field of [
|
|
94
129
|
'contextWindow',
|
|
95
130
|
'compactionThreshold',
|
|
@@ -240,7 +275,7 @@ export function thinkingVariantsForModel(providerType, modelId) {
|
|
|
240
275
|
export function applyModelOverride(model, override) {
|
|
241
276
|
if (!override)
|
|
242
277
|
return model;
|
|
243
|
-
const { thinkingLevels: _thinking, defaultThinkingLevel: _defaultThinking, serviceTier: _tier, compactionThreshold: _threshold, maxOutputTokens: _outputBudget, vision, capabilities, ...facts } = override;
|
|
278
|
+
const { thinkingLevels: _thinking, defaultThinkingLevel: _defaultThinking, serviceTier: _tier, compactionThreshold: _threshold, maxOutputTokens: _outputBudget, applyPatch: _applyPatch, vision, capabilities, ...facts } = override;
|
|
244
279
|
return {
|
|
245
280
|
...model,
|
|
246
281
|
...facts,
|