@mmmbuto/nexuscrew 0.8.45 → 0.8.47
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 +46 -0
- package/README.md +1 -1
- package/frontend/dist/assets/{index-DNFEGdog.js → index-Db2ivuxA.js} +28 -28
- package/frontend/dist/index.html +1 -1
- package/frontend/dist/version.json +1 -1
- package/lib/fleet/builtin.js +19 -2
- package/lib/fleet/cell-exec.js +126 -18
- package/lib/fleet/launch.js +80 -23
- package/lib/fleet/managed.js +53 -5
- package/lib/fleet/prompt-delivery.js +275 -0
- package/lib/fleet/runtime.js +49 -10
- package/package.json +1 -1
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// Delivery del bootstrap prompt per gli engine managed Kimi (kimi.native e
|
|
3
|
+
// claude.kimi-code) — NexusCrew 0.8.47. SOLO questi due engine: gli altri
|
|
4
|
+
// managed conservano il prompt su argv (finding separato) e i custom send-keys
|
|
5
|
+
// conservano injectPrompt legacy senza Enter.
|
|
6
|
+
//
|
|
7
|
+
// Contratto (design audit 0.8.47):
|
|
8
|
+
// - Readiness REALE prima della consegna: il pane viene classificato sul SOLO
|
|
9
|
+
// viewport visibile (capture-pane -p, mai scrollback -S: su generation>0 lo
|
|
10
|
+
// scrollback puo' contenere vecchi prompt e marker stale). Auth, consenso
|
|
11
|
+
// custom API key e onboarding/trust sono NOT_READY: nessun paste, nessun
|
|
12
|
+
// Enter, la sessione resta viva e utilizzabile dall'operatore. UNKNOWN a
|
|
13
|
+
// timeout NON diventa mai READY: la consegna viene saltata.
|
|
14
|
+
// - AT-MOST-ONCE per generazione: UNA sola sequenza paste+Enter. Retry
|
|
15
|
+
// automatico ammesso SOLO su fallimento certo PRE-paste (resolve pane o
|
|
16
|
+
// load-buffer falliti): mai un secondo paste — dopo QUALUNQUE tentativo di
|
|
17
|
+
// paste-buffer il composer potrebbe contenere testo parziale/completo e lo
|
|
18
|
+
// stato e' DELIVERY_UNKNOWN/STAGED_NOT_SUBMITTED con zero secondo paste.
|
|
19
|
+
// Dopo un Enter OK l'esito e' 'submitted' e non esiste replay.
|
|
20
|
+
// - Classifier enum-only: il testo catturato non viene MAI loggato,
|
|
21
|
+
// persistito o incluso in risposte; esce solo lo stato bounded.
|
|
22
|
+
// - Recovery da catalogo costante; mai suggerire /login Anthropic per
|
|
23
|
+
// claude.kimi-code (il recovery corretto e' il consenso Kimi in /config).
|
|
24
|
+
const { tmuxExec, securePaste, resolveSessionPane, promptCharsOk, sleep } = require('./launch.js');
|
|
25
|
+
|
|
26
|
+
// Stati bounded del classifier pane. Output unico esposto all'esterno.
|
|
27
|
+
const PANE_STATES = Object.freeze([
|
|
28
|
+
'ready', 'busy', 'not-ready-auth', 'not-ready-consent', 'not-ready-onboarding', 'unknown',
|
|
29
|
+
]);
|
|
30
|
+
|
|
31
|
+
// Esiti bounded della consegna (prompt.reason in API: closed enum, G5).
|
|
32
|
+
const DELIVERY_STATES = Object.freeze([
|
|
33
|
+
'submitted', // paste ok + pane riverificato + Enter ok
|
|
34
|
+
'staged-not-submitted', // paste ok, Enter fallito: testo forse nel composer
|
|
35
|
+
'delivery-unknown', // paste tentato con esito incerto / pane sparito dopo il paste
|
|
36
|
+
'failed-pre-paste', // resolve/load certamente falliti (anche dopo 1 retry)
|
|
37
|
+
'skipped-not-ready', // classifier not-ready a fine attesa bounded (kind in notReady)
|
|
38
|
+
'skipped-unknown', // classifier mai ready entro il timeout
|
|
39
|
+
'prompt-rejected', // byte di controllo nel prompt (policy §9e)
|
|
40
|
+
'cancelled', // generazione terminata durante l'attesa/consegna (R3)
|
|
41
|
+
'report-timeout', // up() non ha ricevuto l'esito del launcher entro il bound
|
|
42
|
+
]);
|
|
43
|
+
|
|
44
|
+
// Codici bounded actionRequired (G5). Nessuna inferenza "rifiutato": il marker
|
|
45
|
+
// TUI prova solo che serve un'azione nel terminale della cella.
|
|
46
|
+
const ACTION_CODES = Object.freeze(['KIMI_AUTH_ACTION_REQUIRED', 'CLIENT_INTERACTION_REQUIRED']);
|
|
47
|
+
|
|
48
|
+
// Slug recovery bounded (R10): l'API trasporta SOLO {code, recovery}; il testo
|
|
49
|
+
// e' mappato localmente dalla PWA via i18n (fleet-recovery-<slug>), mai inviato
|
|
50
|
+
// dal server (un nodo remoto federato non deve poter iniettare testo libero).
|
|
51
|
+
const RECOVERY_SLUGS = Object.freeze([
|
|
52
|
+
'kimi-code-consent-yes',
|
|
53
|
+
'kimi-code-config-custom-api-key',
|
|
54
|
+
'kimi-cli-login',
|
|
55
|
+
'client-terminal-dialog',
|
|
56
|
+
]);
|
|
57
|
+
|
|
58
|
+
// Marker di dialoghi MODALI full-screen: occupano la viewport, valgono su
|
|
59
|
+
// tutto il catturato visibile (non solo la coda).
|
|
60
|
+
// Live-verificati: claude 2.1.220 (consent custom API key, trust dialog).
|
|
61
|
+
const MODAL_NOT_READY = Object.freeze([
|
|
62
|
+
['not-ready-consent', /Detected a custom API key in your environment|Do you want to use this API key\?/],
|
|
63
|
+
['not-ready-onboarding', /Yes, I trust this folder|Quick safety check/],
|
|
64
|
+
]);
|
|
65
|
+
|
|
66
|
+
// Marker ancorati alla CODA visibile (status bar / righe finali): lo stesso
|
|
67
|
+
// testo in scrollback, nel corpo conversazione o in forma diversa non deve
|
|
68
|
+
// classificare (R7: il marker auth e' la stringa ESATTA di status bar, non un
|
|
69
|
+
// "Not logged in" qualunque). Live-verificati: claude 2.1.220 (status bar
|
|
70
|
+
// "Not logged in · Run /login"), kimi 0.31.1 (welcome "Run /login or
|
|
71
|
+
// /provider", "Model: not set").
|
|
72
|
+
const TAIL_NOT_READY = Object.freeze([
|
|
73
|
+
['not-ready-auth', /Not logged in · (?:Please run|Run) \/login\b/],
|
|
74
|
+
['not-ready-auth', /Run \/login or \/provider|Model:\s+not set\b/],
|
|
75
|
+
]);
|
|
76
|
+
|
|
77
|
+
// TUI occupata a processare (restart durante lavoro residuo): si attende.
|
|
78
|
+
const BUSY_TAIL = /esc to interrupt/i;
|
|
79
|
+
|
|
80
|
+
const TAIL_LINES = 14;
|
|
81
|
+
|
|
82
|
+
function tailOf(text) {
|
|
83
|
+
const lines = String(text).split('\n');
|
|
84
|
+
const nonEmpty = [];
|
|
85
|
+
for (let i = lines.length - 1; i >= 0 && nonEmpty.length < TAIL_LINES; i -= 1) {
|
|
86
|
+
if (lines[i].trim() !== '') nonEmpty.unshift(lines[i]);
|
|
87
|
+
}
|
|
88
|
+
return nonEmpty.join('\n');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// classifyPane(captured, client) -> PANE_STATES enum. Pura, senza dipendenze:
|
|
92
|
+
// il testo resta dentro la funzione, fuori esce solo lo stato bounded.
|
|
93
|
+
// client: 'kimi' | 'claude' (gli altri non passano mai di qui: 'unknown').
|
|
94
|
+
function classifyPane(captured, client) {
|
|
95
|
+
const text = typeof captured === 'string' ? captured : '';
|
|
96
|
+
if (!text.trim()) return 'unknown';
|
|
97
|
+
for (const [state, re] of MODAL_NOT_READY) if (re.test(text)) return state;
|
|
98
|
+
const tail = tailOf(text);
|
|
99
|
+
for (const [state, re] of TAIL_NOT_READY) if (re.test(tail)) return state;
|
|
100
|
+
if (BUSY_TAIL.test(tail)) return 'busy';
|
|
101
|
+
if (client === 'kimi') {
|
|
102
|
+
// READY positivo Kimi: box di input corrente E modello configurato (G2).
|
|
103
|
+
// Il solo "Model:" non basta; la sola box non basta (presente anche da
|
|
104
|
+
// logged-out). Il welcome logged-out e' gia' intercettato dai marker
|
|
105
|
+
// not-ready qui sopra; se scrolla via senza login resta 'unknown' (safe).
|
|
106
|
+
const box = /^\s*│\s*>/m.test(text);
|
|
107
|
+
const model = /! to run a shell command/.test(tail) || /Model:\s+(?!not set\b)\S/.test(text);
|
|
108
|
+
return box && model ? 'ready' : 'unknown';
|
|
109
|
+
}
|
|
110
|
+
if (client === 'claude') {
|
|
111
|
+
// READY positivo Claude: riga prompt "❯" in coda, assenza dei marker
|
|
112
|
+
// not-ready (gia' valutati sopra: il cursore ❯ dei dialoghi non inganna).
|
|
113
|
+
return /^\s*❯/m.test(tail) ? 'ready' : 'unknown';
|
|
114
|
+
}
|
|
115
|
+
return 'unknown';
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function clampInt(value, dflt, min, max) {
|
|
119
|
+
const n = Number(value);
|
|
120
|
+
if (!Number.isFinite(n)) return dflt;
|
|
121
|
+
return Math.max(min, Math.min(max, Math.trunc(n)));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// deliverBootstrapPrompt(opts) -> { delivered, state, notReady, attempts, reason }
|
|
125
|
+
// opts: { tmuxBin, session, prompt, client ('kimi'|'claude'), env,
|
|
126
|
+
// paneTarget (%N opzionale), readyWaitMs, pollMs, settleMs, tmpdir,
|
|
127
|
+
// tmuxExecImpl, captureImpl, sleepImpl, nowImpl, fsImpl, isCancelled }
|
|
128
|
+
// reason e' SEMPRE lo state (closed enum); notReady e' il kind bounded o ''.
|
|
129
|
+
// isCancelled() (R3): valutato ad OGNI poll, prima del paste e prima dell'
|
|
130
|
+
// Enter; se true la consegna si ferma subito ('cancelled') senza paste/Enter —
|
|
131
|
+
// cell-exec cancella la delivery quando la generazione termina, cosi' un
|
|
132
|
+
// polling in volo non puo' mai iniettare nella generazione successiva.
|
|
133
|
+
async function deliverBootstrapPrompt(opts = {}) {
|
|
134
|
+
const { tmuxBin, session, prompt } = opts;
|
|
135
|
+
const client = typeof opts.client === 'string' ? opts.client : '';
|
|
136
|
+
const env = opts.env;
|
|
137
|
+
const exec = opts.tmuxExecImpl || tmuxExec;
|
|
138
|
+
const sleepImpl = opts.sleepImpl || sleep;
|
|
139
|
+
const now = opts.nowImpl || Date.now;
|
|
140
|
+
const cancelled = typeof opts.isCancelled === 'function' ? opts.isCancelled : () => false;
|
|
141
|
+
const target = opts.paneTarget || `=${session}`;
|
|
142
|
+
const readyWaitMs = clampInt(opts.readyWaitMs, 15000, 0, 120000);
|
|
143
|
+
const pollMs = clampInt(opts.pollMs, 400, 50, 5000);
|
|
144
|
+
const settleMs = clampInt(opts.settleMs, 150, 0, 5000);
|
|
145
|
+
const done = (delivered, state, notReady, attempts) => ({
|
|
146
|
+
delivered, state, notReady, attempts, reason: state,
|
|
147
|
+
});
|
|
148
|
+
if (!promptCharsOk(prompt)) return done(false, 'prompt-rejected', '', 0);
|
|
149
|
+
|
|
150
|
+
const capture = opts.captureImpl || (async () => {
|
|
151
|
+
const r = await exec(tmuxBin, ['capture-pane', '-p', '-t', target], { env, timeoutMs: 2000 });
|
|
152
|
+
return r.err ? null : r.stdout;
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// (1) Attesa readiness bounded: solo 'ready' positivo sblocca la consegna.
|
|
156
|
+
let paneState = 'unknown';
|
|
157
|
+
const deadline = now() + readyWaitMs;
|
|
158
|
+
for (;;) {
|
|
159
|
+
if (cancelled()) return done(false, 'cancelled', '', 0);
|
|
160
|
+
const text = await capture();
|
|
161
|
+
if (cancelled()) return done(false, 'cancelled', '', 0);
|
|
162
|
+
paneState = text === null ? 'unknown' : classifyPane(text, client);
|
|
163
|
+
if (paneState === 'ready') break;
|
|
164
|
+
if (now() >= deadline) {
|
|
165
|
+
return paneState === 'unknown'
|
|
166
|
+
? done(false, 'skipped-unknown', '', 0)
|
|
167
|
+
: done(false, 'skipped-not-ready', paneState, 0);
|
|
168
|
+
}
|
|
169
|
+
await sleepImpl(pollMs);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// (2) Consegna AT-MOST-ONCE: un solo paste per generazione. Il retry copre
|
|
173
|
+
// SOLO fallimenti certi pre-paste (resolve/load); dopo qualsiasi tentativo
|
|
174
|
+
// di paste-buffer non esiste retry automatico (G1).
|
|
175
|
+
let attempts = 0;
|
|
176
|
+
for (;;) {
|
|
177
|
+
if (cancelled()) return done(false, 'cancelled', '', attempts);
|
|
178
|
+
attempts += 1;
|
|
179
|
+
const stage = await securePaste(tmuxBin, session, prompt, {
|
|
180
|
+
env, exec, target, tmpdir: opts.tmpdir, fsImpl: opts.fsImpl,
|
|
181
|
+
});
|
|
182
|
+
if (cancelled()) {
|
|
183
|
+
// Il paste potrebbe essere avvenuto: mai riprovare, mai Enter.
|
|
184
|
+
return done(false, stage.ok ? 'delivery-unknown' : 'cancelled', '', attempts);
|
|
185
|
+
}
|
|
186
|
+
if (stage.ok) {
|
|
187
|
+
await sleepImpl(settleMs);
|
|
188
|
+
if (cancelled()) return done(false, 'delivery-unknown', '', attempts);
|
|
189
|
+
// Riverifica dello stesso %N sulla stessa sessione prima dell'Enter.
|
|
190
|
+
const paneAgain = await resolveSessionPane(tmuxBin, session, { env, exec, target });
|
|
191
|
+
if (paneAgain !== stage.paneId) return done(false, 'delivery-unknown', '', attempts);
|
|
192
|
+
if (cancelled()) return done(false, 'delivery-unknown', '', attempts);
|
|
193
|
+
const enter = await exec(tmuxBin, ['send-keys', '-t', stage.paneId, 'Enter'], { env });
|
|
194
|
+
if (enter.err) return done(false, 'staged-not-submitted', '', attempts);
|
|
195
|
+
// R9: cancel con Enter appena partito -> la generazione e' morta con un
|
|
196
|
+
// submit in volo: esito incerto (residuo PTY possibile), mai 'submitted'.
|
|
197
|
+
if (cancelled()) return done(false, 'delivery-unknown', '', attempts);
|
|
198
|
+
return done(true, 'submitted', '', attempts);
|
|
199
|
+
}
|
|
200
|
+
if (stage.stage === 'paste' || stage.stage === 'validate') {
|
|
201
|
+
// Paste tentato (esito incerto) o input rifiutato: zero secondo paste.
|
|
202
|
+
return done(false, stage.stage === 'validate' ? 'prompt-rejected' : 'delivery-unknown', '', attempts);
|
|
203
|
+
}
|
|
204
|
+
if (attempts >= 2) return done(false, 'failed-pre-paste', '', attempts);
|
|
205
|
+
// Fallimento certo PRE-paste: un solo retry, readiness riverificata prima.
|
|
206
|
+
await sleepImpl(pollMs);
|
|
207
|
+
if (cancelled()) return done(false, 'cancelled', '', attempts);
|
|
208
|
+
const text = await capture();
|
|
209
|
+
const again = text === null ? 'unknown' : classifyPane(text, client);
|
|
210
|
+
if (again !== 'ready') return done(false, 'failed-pre-paste', '', attempts);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// waitDeliveryReport(tmuxBin, target, opts) -> delivery-like | null (R2).
|
|
215
|
+
// Il launcher supervisionato (cell-exec) e' l'UNICO owner della consegna per
|
|
216
|
+
// TUTTE le generazioni degli engine Kimi e pubblica l'esito bounded sull'
|
|
217
|
+
// opzione tmux di pane @nc_delivery ('<state>' o '<state>:<notReady>', solo
|
|
218
|
+
// enum chiusi; muore col pane, nessuno state file). up() la legge con attesa
|
|
219
|
+
// bounded: niente paste dal runtime, niente doppio writer cross-generation.
|
|
220
|
+
async function waitDeliveryReport(tmuxBin, target, { env, exec, sleepImpl, nowImpl, timeoutMs, pollMs } = {}) {
|
|
221
|
+
const run = exec || tmuxExec;
|
|
222
|
+
const sleepFn = sleepImpl || sleep;
|
|
223
|
+
const now = nowImpl || Date.now;
|
|
224
|
+
const deadline = now() + clampInt(timeoutMs, 18000, 100, 150000);
|
|
225
|
+
const step = clampInt(pollMs, 300, 50, 5000);
|
|
226
|
+
for (;;) {
|
|
227
|
+
const r = await run(tmuxBin,
|
|
228
|
+
['display-message', '-p', '-t', target, '#{@nc_delivery}'],
|
|
229
|
+
{ env, timeoutMs: 2000 });
|
|
230
|
+
const raw = r.err ? '' : r.stdout.trim();
|
|
231
|
+
if (raw) {
|
|
232
|
+
const [state, notReady = ''] = raw.split(':');
|
|
233
|
+
if (!DELIVERY_STATES.includes(state)) return null; // valore non bounded: mai fidarsi
|
|
234
|
+
const kind = PANE_STATES.includes(notReady) ? notReady : '';
|
|
235
|
+
return {
|
|
236
|
+
delivered: state === 'submitted',
|
|
237
|
+
state, notReady: state === 'skipped-not-ready' ? kind : '',
|
|
238
|
+
attempts: 0, reason: state,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
if (now() >= deadline) return null;
|
|
242
|
+
await sleepFn(step);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// actionRequiredFor(client, provider, delivery) -> null | { code, recovery }
|
|
247
|
+
// Mappa bounded delivery -> azione operatore. Solo skip per not-ready/unknown
|
|
248
|
+
// producono actionRequired; i fallimenti di trasporto restano nel prompt.state.
|
|
249
|
+
// R10: SOLO {code, recovery} closed enum/slug — il testo e' i18n locale PWA.
|
|
250
|
+
function actionRequiredFor(client, provider, delivery) {
|
|
251
|
+
if (!delivery || delivery.delivered) return null;
|
|
252
|
+
if (delivery.state !== 'skipped-not-ready' && delivery.state !== 'skipped-unknown') return null;
|
|
253
|
+
const kind = delivery.notReady;
|
|
254
|
+
let recovery = 'client-terminal-dialog';
|
|
255
|
+
let code = 'CLIENT_INTERACTION_REQUIRED';
|
|
256
|
+
if (provider === 'kimi-code') {
|
|
257
|
+
code = kind === 'not-ready-consent' || kind === 'not-ready-auth'
|
|
258
|
+
? 'KIMI_AUTH_ACTION_REQUIRED' : 'CLIENT_INTERACTION_REQUIRED';
|
|
259
|
+
recovery = kind === 'not-ready-consent' ? 'kimi-code-consent-yes'
|
|
260
|
+
: kind === 'not-ready-auth' ? 'kimi-code-config-custom-api-key'
|
|
261
|
+
: 'client-terminal-dialog';
|
|
262
|
+
} else if (client === 'kimi') {
|
|
263
|
+
if (kind === 'not-ready-auth') {
|
|
264
|
+
code = 'KIMI_AUTH_ACTION_REQUIRED';
|
|
265
|
+
recovery = 'kimi-cli-login';
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
if (!ACTION_CODES.includes(code) || !RECOVERY_SLUGS.includes(recovery)) return null;
|
|
269
|
+
return { code, recovery };
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
module.exports = {
|
|
273
|
+
PANE_STATES, DELIVERY_STATES, ACTION_CODES, RECOVERY_SLUGS,
|
|
274
|
+
classifyPane, deliverBootstrapPrompt, waitDeliveryReport, actionRequiredFor,
|
|
275
|
+
};
|
package/lib/fleet/runtime.js
CHANGED
|
@@ -25,6 +25,7 @@ const {
|
|
|
25
25
|
waitAlive, waitStablePane, injectPrompt,
|
|
26
26
|
redactSecrets, sanitizeEarlyDiagnostic,
|
|
27
27
|
} = require('./launch.js');
|
|
28
|
+
const { waitDeliveryReport, actionRequiredFor } = require('./prompt-delivery.js');
|
|
28
29
|
|
|
29
30
|
// TTL della cache status (ms): scaduto, status rilegge tmux + defs da disco.
|
|
30
31
|
const STATUS_TTL_MS = 2000;
|
|
@@ -209,6 +210,12 @@ function createBuiltinRuntime(ctx) {
|
|
|
209
210
|
tmuxSession: cell.tmuxSession,
|
|
210
211
|
prompt: cell.prompt,
|
|
211
212
|
readyMs: Math.max(0, Math.min(30000, Number(cfg.sendKeysReadyMs) || readyMs)),
|
|
213
|
+
// Solo gli engine managed Kimi (kimi.native / claude.kimi-code) hanno
|
|
214
|
+
// la delivery classificata at-most-once ai restart; gli engine custom
|
|
215
|
+
// send-keys conservano il reinject legacy senza Enter (0.8.47 G4).
|
|
216
|
+
client: engine.managed && engine.managed.client === 'kimi' ? 'kimi'
|
|
217
|
+
: (engine.managed && engine.managed.provider === 'kimi-code' ? 'claude' : ''),
|
|
218
|
+
readyWaitMs: Math.max(0, Math.min(120000, Number(cfg.bootstrapReadyWaitMs) || 15000)),
|
|
212
219
|
},
|
|
213
220
|
} : {}),
|
|
214
221
|
});
|
|
@@ -396,20 +403,52 @@ function createBuiltinRuntime(ctx) {
|
|
|
396
403
|
};
|
|
397
404
|
}
|
|
398
405
|
|
|
399
|
-
// (6) prompt
|
|
400
|
-
//
|
|
406
|
+
// (6) prompt: due percorsi distinti (0.8.47, R2 single-owner).
|
|
407
|
+
// - managed kimi.native / claude.kimi-code: la consegna e' posseduta SOLO
|
|
408
|
+
// dal supervisore (cell-exec) per TUTTE le generazioni; qui si legge
|
|
409
|
+
// l'esito bounded (@nc_delivery sul pane) con attesa bounded. Mai paste/
|
|
410
|
+
// Enter dal runtime: niente doppia delivery se gen0 muore durante
|
|
411
|
+
// l'attesa e gen1 parte sotto il supervisore.
|
|
412
|
+
// - engine custom promptMode 'send-keys': injectPrompt legacy (bracketed
|
|
413
|
+
// paste SENZA Enter, contratto invariato — G4), target %N esatto.
|
|
401
414
|
let prompt = null;
|
|
415
|
+
let actionRequired = null;
|
|
402
416
|
if (launchEngine.promptMode === 'send-keys' && cell.prompt) {
|
|
403
|
-
const
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
417
|
+
const managedClient = engine.managed && typeof engine.managed.client === 'string' ? engine.managed.client : '';
|
|
418
|
+
const managedProvider = engine.managed && typeof engine.managed.provider === 'string' ? engine.managed.provider : '';
|
|
419
|
+
const classified = managedClient === 'kimi' || managedProvider === 'kimi-code';
|
|
420
|
+
if (classified) {
|
|
421
|
+
const readyWaitMs = Math.max(0, Math.min(120000, Number(cfg.bootstrapReadyWaitMs) || 15000));
|
|
422
|
+
const reportWaitMs = Math.max(100, Math.min(150000,
|
|
423
|
+
(Number(cfg.sendKeysReadyMs) || readyMs) + readyWaitMs + 2000));
|
|
424
|
+
const report = await waitDeliveryReport(tmuxBin,
|
|
425
|
+
paneId.startsWith('%') ? paneId : `=${cell.tmuxSession}`,
|
|
426
|
+
{ env: minimalEnv(), timeoutMs: reportWaitMs });
|
|
427
|
+
const delivery = report || {
|
|
428
|
+
delivered: false, state: 'report-timeout', notReady: '', attempts: 0, reason: 'report-timeout',
|
|
429
|
+
};
|
|
430
|
+
prompt = {
|
|
431
|
+
injected: delivery.delivered,
|
|
432
|
+
delivered: delivery.delivered,
|
|
433
|
+
state: delivery.state,
|
|
434
|
+
reason: delivery.reason,
|
|
435
|
+
};
|
|
436
|
+
actionRequired = actionRequiredFor(managedClient, managedProvider, delivery);
|
|
437
|
+
} else {
|
|
438
|
+
const target = paneId.startsWith('%') ? paneId : `=${cell.tmuxSession}`;
|
|
439
|
+
prompt = await injectPrompt(tmuxBin, cell.tmuxSession, cell.prompt, {
|
|
440
|
+
env: minimalEnv(),
|
|
441
|
+
readyMs: cfg.sendKeysReadyMs != null ? cfg.sendKeysReadyMs : readyMs,
|
|
442
|
+
target,
|
|
443
|
+
engine: launchEngine, cell, // per la redazione del reason se paste-buffer fallisce (§9h)
|
|
444
|
+
});
|
|
445
|
+
}
|
|
410
446
|
}
|
|
411
447
|
cache = { ...cache, at: 0 }; // invalida: prossimo status rilegge tmux
|
|
412
|
-
return {
|
|
448
|
+
return {
|
|
449
|
+
ok: true, cell: cellId, session: cell.tmuxSession, prompt,
|
|
450
|
+
...(actionRequired ? { actionRequired } : {}),
|
|
451
|
+
};
|
|
413
452
|
}
|
|
414
453
|
|
|
415
454
|
async function down(cellId /* , opts */) {
|