@nac3/forge-cli 1.17.0 → 1.18.0
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/dist/bin/heap_bootstrap.d.ts +52 -1
- package/dist/bin/heap_bootstrap.d.ts.map +1 -1
- package/dist/bin/heap_bootstrap.js +46 -11
- package/dist/bin/heap_bootstrap.js.map +1 -1
- package/dist/chat/claude.d.ts +7 -0
- package/dist/chat/claude.d.ts.map +1 -1
- package/dist/chat/claude.js.map +1 -1
- package/dist/chat/claude_cli.d.ts +4 -0
- package/dist/chat/claude_cli.d.ts.map +1 -1
- package/dist/chat/claude_cli.js +58 -0
- package/dist/chat/claude_cli.js.map +1 -1
- package/dist/chat/server.d.ts.map +1 -1
- package/dist/chat/server.js +95 -14
- package/dist/chat/server.js.map +1 -1
- package/dist/chat/turn_control.d.ts +66 -19
- package/dist/chat/turn_control.d.ts.map +1 -1
- package/dist/chat/turn_control.js +112 -31
- package/dist/chat/turn_control.js.map +1 -1
- package/dist/core/limits_config.d.ts +15 -0
- package/dist/core/limits_config.d.ts.map +1 -1
- package/dist/core/limits_config.js +5 -0
- package/dist/core/limits_config.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/chat/server.js
CHANGED
|
@@ -55,7 +55,7 @@ import { relayPoll } from '../relay/relay_client.js';
|
|
|
55
55
|
import { startCommandConsumer } from '../relay/command_consumer.js';
|
|
56
56
|
import { getSessionStore, DEFAULT_SESSION_ID } from './session_store.js';
|
|
57
57
|
import { getSessionProject, setSessionProject } from './session_projects.js';
|
|
58
|
-
import { beginTurn, endTurn, interruptActiveTurn, isTurnActive, activeTurnId } from './turn_control.js';
|
|
58
|
+
import { beginTurn, endTurn, interruptActiveTurn, interruptAllTurns, isTurnActive, activeTurnId, activeTurns, } from './turn_control.js';
|
|
59
59
|
import { openSteering, closeSteering, drainSteers, formatSteer, queueSteer, pendingSteers, } from './turn_steering.js';
|
|
60
60
|
import { manifest as nac3Manifest } from '../nac3/internal_manifest.js';
|
|
61
61
|
import { buildManifestForPrompt } from '../nac3/manifest_lazy.js';
|
|
@@ -334,9 +334,11 @@ export async function startChatServer(opts) {
|
|
|
334
334
|
* claude_cli_timeout_ms <ms>` or the panel. */
|
|
335
335
|
try {
|
|
336
336
|
const { readLimitsConfig } = await import('../core/limits_config.js');
|
|
337
|
-
|
|
337
|
+
const _lim = await readLimitsConfig();
|
|
338
|
+
cliOpts['timeoutMs'] = _lim.claude_cli_timeout_ms;
|
|
339
|
+
cliOpts['silenceMs'] = _lim.claude_cli_silence_ms;
|
|
338
340
|
}
|
|
339
|
-
catch { /* fall back to the ClaudeCliClient
|
|
341
|
+
catch { /* fall back to the ClaudeCliClient defaults (30 min / 15 min) */ }
|
|
340
342
|
/* Wire the panel MCP bridge: the subprocess gets the forge_panel
|
|
341
343
|
* MCP server so it can create pizarron tabs. */
|
|
342
344
|
cliOpts['panelBridge'] = { port: opts.port, token: panelPushToken };
|
|
@@ -1394,6 +1396,36 @@ function stopRelayHeartbeat() {
|
|
|
1394
1396
|
_relayHeartbeat = null;
|
|
1395
1397
|
}
|
|
1396
1398
|
}
|
|
1399
|
+
/**
|
|
1400
|
+
* El turno que esta corriendo pero que el anuncio del relay no cubre.
|
|
1401
|
+
*
|
|
1402
|
+
* `setInFlightTurn` se saltea A PROPOSITO los turnos de origen mobile y
|
|
1403
|
+
* bridge: el telefono ya pinta su propia burbuja y anunciarlos de vuelta la
|
|
1404
|
+
* duplicaba. El efecto secundario se vio el 14-oct-2026: un turno pedido
|
|
1405
|
+
* DESDE el telefono es invisible para el panel del escritorio. Pablo mando
|
|
1406
|
+
* "Adelante", el subproceso corrio veinte minutos escribiendo en el arbol, y
|
|
1407
|
+
* la ventana del panel contestaba `in_flight: null` todo el tiempo.
|
|
1408
|
+
*
|
|
1409
|
+
* El registro de turnos vivos (turn_control) si los tiene a todos, sin
|
|
1410
|
+
* importar de donde vinieron. Cuando el anuncio no tiene nada, se cae ahi y
|
|
1411
|
+
* se reporta el MAS VIEJO: entre varios, el que lleva mas rato sin que nadie
|
|
1412
|
+
* lo vea es el que hay que mostrar.
|
|
1413
|
+
*/
|
|
1414
|
+
function turnoVivoSinAnunciar() {
|
|
1415
|
+
try {
|
|
1416
|
+
const t = activeTurns()[0];
|
|
1417
|
+
if (!t)
|
|
1418
|
+
return null;
|
|
1419
|
+
return {
|
|
1420
|
+
id: t.id,
|
|
1421
|
+
user: t.user ?? '',
|
|
1422
|
+
started_at: new Date(t.startedAt).toISOString(),
|
|
1423
|
+
};
|
|
1424
|
+
}
|
|
1425
|
+
catch {
|
|
1426
|
+
return null;
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1397
1429
|
function startRelayHeartbeat() {
|
|
1398
1430
|
stopRelayHeartbeat();
|
|
1399
1431
|
_relayHeartbeat = setInterval(() => {
|
|
@@ -1588,10 +1620,24 @@ async function route(req, res, ctx) {
|
|
|
1588
1620
|
* abort the active turn's signal so the caller unwinds (claude.ts
|
|
1589
1621
|
* breaks at the next round boundary; claude_cli kills `claude -p`).
|
|
1590
1622
|
* Idempotent: returns active:false when nothing is running. */
|
|
1623
|
+
/* `?all=1` corta TODOS los turnos vivos, no solo el ultimo. Es la salida
|
|
1624
|
+
* para el turno que se perdio de vista: el 14-oct un turno del telefono
|
|
1625
|
+
* quedo corriendo 20 minutos detras de otro mas nuevo, y "Detener" no lo
|
|
1626
|
+
* alcanzaba porque apuntaba al de arriba. `live` viaja siempre para que
|
|
1627
|
+
* quien llama pueda DECIR cuantos hay en vez de adivinar. */
|
|
1591
1628
|
if (req.method === 'POST' && url.pathname === '/api/chat/interrupt') {
|
|
1629
|
+
const vivos = activeTurns();
|
|
1592
1630
|
const wasActive = isTurnActive();
|
|
1593
|
-
const
|
|
1594
|
-
|
|
1631
|
+
const todos = url.searchParams.get('all') === '1';
|
|
1632
|
+
const r = todos ? interruptAllTurns() : interruptActiveTurn();
|
|
1633
|
+
sendJson(res, 200, {
|
|
1634
|
+
ok: true,
|
|
1635
|
+
active: wasActive,
|
|
1636
|
+
interrupted: r.ok,
|
|
1637
|
+
turn_id: 'id' in r ? r.id : undefined,
|
|
1638
|
+
turn_ids: 'ids' in r ? r.ids : undefined,
|
|
1639
|
+
live: vivos.length,
|
|
1640
|
+
});
|
|
1595
1641
|
return;
|
|
1596
1642
|
}
|
|
1597
1643
|
/* Steering mid-turn (2026-09-29) -- el usuario escribio con un turno en
|
|
@@ -2234,7 +2280,7 @@ async function route(req, res, ctx) {
|
|
|
2234
2280
|
const { redactDeep } = await import('../vault/redactor.js');
|
|
2235
2281
|
sendJson(res, 200, construirRestore({
|
|
2236
2282
|
messages: store.messages(),
|
|
2237
|
-
inFlight: inFlightTurn(),
|
|
2283
|
+
inFlight: inFlightTurn() ?? turnoVivoSinAnunciar(),
|
|
2238
2284
|
projectRoot: ctx.projectRoot,
|
|
2239
2285
|
sessionStartedAt: store.startedAt(),
|
|
2240
2286
|
redactar: (s) => String(redactDeep(s)),
|
|
@@ -5707,6 +5753,33 @@ async function handleChat(req, res, ctx) {
|
|
|
5707
5753
|
const pilotState = await readPilotState(ctx.projectRoot);
|
|
5708
5754
|
const lastUserMsg = normalized.filter((m) => m.role === 'user').slice(-1)[0];
|
|
5709
5755
|
const lastUserText = typeof lastUserMsg?.content === 'string' ? lastUserMsg.content : '';
|
|
5756
|
+
/* El pedido entra al transcript AHORA, no al final del turno (Pablo,
|
|
5757
|
+
* 14-oct-2026).
|
|
5758
|
+
*
|
|
5759
|
+
* Antes el mensaje del usuario se grababa recien despues de tener la
|
|
5760
|
+
* respuesta, junto con ella. Un turno que no llegaba a contestar no
|
|
5761
|
+
* dejaba NADA: ni el pedido. El 14-oct un "Adelante" del telefono corrio
|
|
5762
|
+
* veinte minutos y despues se perdio sin rastro -- `turn_routing.jsonl`
|
|
5763
|
+
* tenia seis turnos ese dia y el transcript cinco mensajes de usuario, y
|
|
5764
|
+
* el que faltaba era justo el que habia fallado. Para saber que existio
|
|
5765
|
+
* hubo que cruzar tres logs.
|
|
5766
|
+
*
|
|
5767
|
+
* Un pedido sin respuesta es un transcript honesto: dice que se pidio y
|
|
5768
|
+
* que no se contesto. Perderlo es mentir por omision. El append de abajo
|
|
5769
|
+
* (el del cierre del turno) sigue estando y queda como no-op por su
|
|
5770
|
+
* propia guarda de duplicado; es la red para cualquier camino que no
|
|
5771
|
+
* pase por aca. */
|
|
5772
|
+
{
|
|
5773
|
+
const _nuevo = normalized[normalized.length - 1];
|
|
5774
|
+
if (_nuevo && _nuevo.role === 'user') {
|
|
5775
|
+
const _previos = sessionStore.messages();
|
|
5776
|
+
const _cola = _previos[_previos.length - 1];
|
|
5777
|
+
if (!_cola || _cola.role !== 'user' || _cola.content !== _nuevo.content) {
|
|
5778
|
+
sessionStore.append(_nuevo);
|
|
5779
|
+
void sessionStore.flush();
|
|
5780
|
+
}
|
|
5781
|
+
}
|
|
5782
|
+
}
|
|
5710
5783
|
/* Immediate cross-device ack (PND -- "duda si esta trabajando"). The
|
|
5711
5784
|
* RECEIVING device must see the incoming message + a working state the
|
|
5712
5785
|
* instant the turn STARTS, not when it finishes. Shared id correlates
|
|
@@ -6106,11 +6179,14 @@ async function handleChat(req, res, ctx) {
|
|
|
6106
6179
|
/* Register THIS turn as the active one right before the model
|
|
6107
6180
|
* call. beginTurn returns the AbortSignal the callers honor
|
|
6108
6181
|
* (claude.ts at round boundary, claude_cli kills the subprocess).
|
|
6109
|
-
*
|
|
6110
|
-
*
|
|
6111
|
-
*
|
|
6182
|
+
* Un turno concurrente (desktop + sesion propia del movil) ya NO pisa
|
|
6183
|
+
* al anterior en el registro: los dos quedan vivos y `interrupt` apunta
|
|
6184
|
+
* al mas reciente, pero el viejo sigue alcanzable (interruptAllTurns).
|
|
6185
|
+
* Pisarlo era lo que dejaba subprocesos huerfanos el 14-oct-2026. */
|
|
6112
6186
|
turnId = sessionId + ':' + String(Date.now());
|
|
6113
|
-
|
|
6187
|
+
/* El texto viaja al registro para que un turno que se pierda de vista se
|
|
6188
|
+
* pueda MOSTRAR y no solo contar (ver turnoVivoSinAnunciar). */
|
|
6189
|
+
const turnSignal = beginTurn(turnId, undefined, lastUserText);
|
|
6114
6190
|
/* Steering mid-turn: desde aca el panel puede depositar correcciones
|
|
6115
6191
|
* que el turno retira en su proxima frontera de ronda. */
|
|
6116
6192
|
openSteering(turnId);
|
|
@@ -6223,6 +6299,11 @@ async function handleChat(req, res, ctx) {
|
|
|
6223
6299
|
* seguia avisando 5000000. Sale del mismo `_limits` que ya se lee cada
|
|
6224
6300
|
* turno para max_tokens_per_turn. Los clientes HTTP lo ignoran. */
|
|
6225
6301
|
timeoutMs: _limits.claude_cli_timeout_ms,
|
|
6302
|
+
/* Tope de SILENCIO, tambien por turno. El de reloj esta puesto alto a
|
|
6303
|
+
* proposito (turnos autonomos de horas); este es el que corta al hijo
|
|
6304
|
+
* que se quedo mudo sin dejar cuatro horas de proceso colgado en el
|
|
6305
|
+
* arbol del proyecto (Pablo, 14-oct-2026). */
|
|
6306
|
+
silenceMs: _limits.claude_cli_silence_ms,
|
|
6226
6307
|
/* Plan/CLI uses Claude Code's native tools (FORGE_TOOL_SPECS = just
|
|
6227
6308
|
* nac3_invoke via the MCP bridge); the HTTP path (BYOK/Console + managed)
|
|
6228
6309
|
* gets direct native file tools + nac3_invoke so the model is fluent with
|
|
@@ -6564,10 +6645,10 @@ async function handleChat(req, res, ctx) {
|
|
|
6564
6645
|
}
|
|
6565
6646
|
catch { /* best-effort */ }
|
|
6566
6647
|
}
|
|
6567
|
-
//
|
|
6568
|
-
//
|
|
6569
|
-
//
|
|
6570
|
-
//
|
|
6648
|
+
// El mensaje del usuario YA se grabo al entrar el turno (ver el bloque
|
|
6649
|
+
// "El pedido entra al transcript AHORA" mas arriba), asi que este append
|
|
6650
|
+
// normalmente no hace nada: queda como red para cualquier camino que no
|
|
6651
|
+
// haya pasado por alla. Lo que si se graba aca es la respuesta.
|
|
6571
6652
|
const lastUser = normalized[normalized.length - 1];
|
|
6572
6653
|
if (lastUser && lastUser.role === 'user') {
|
|
6573
6654
|
// Only append if this user message isn't already the last
|