@nac3/forge-cli 1.23.2 → 1.23.4
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/chat/server.d.ts.map +1 -1
- package/dist/chat/server.js +108 -27
- package/dist/chat/server.js.map +1 -1
- package/dist/core/error_catalog.d.ts +27 -0
- package/dist/core/error_catalog.d.ts.map +1 -1
- package/dist/core/error_catalog.js +55 -0
- package/dist/core/error_catalog.js.map +1 -1
- package/dist/core/error_formatter.d.ts +12 -0
- package/dist/core/error_formatter.d.ts.map +1 -1
- package/dist/core/error_formatter.js +19 -0
- package/dist/core/error_formatter.js.map +1 -1
- package/dist/relay/command_consumer.d.ts +10 -0
- package/dist/relay/command_consumer.d.ts.map +1 -1
- package/dist/relay/command_consumer.js +10 -1
- package/dist/relay/command_consumer.js.map +1 -1
- package/dist/relay/in_flight_turn.d.ts +52 -11
- package/dist/relay/in_flight_turn.d.ts.map +1 -1
- package/dist/relay/in_flight_turn.js +98 -30
- package/dist/relay/in_flight_turn.js.map +1 -1
- package/dist/relay/loopback_turn.d.ts +82 -0
- package/dist/relay/loopback_turn.d.ts.map +1 -0
- package/dist/relay/loopback_turn.js +171 -0
- package/dist/relay/loopback_turn.js.map +1 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/src/i18n/catalogs/ar.json +7 -0
- package/src/i18n/catalogs/de.json +7 -0
- package/src/i18n/catalogs/en.json +7 -0
- package/src/i18n/catalogs/es.json +7 -0
- package/src/i18n/catalogs/fr.json +7 -0
- package/src/i18n/catalogs/hi.json +7 -0
- package/src/i18n/catalogs/it.json +7 -0
- package/src/i18n/catalogs/ja.json +7 -0
- package/src/i18n/catalogs/pt.json +7 -0
- package/src/i18n/catalogs/zh.json +7 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/chat/server.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,EAA2D,KAAK,MAAM,EAAE,MAAM,WAAW,CAAC;AAKjG,OAAO,EAAoD,KAAK,aAAa,EAAoB,MAAM,aAAa,CAAC;AAErH,OAAO,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAInD,OAAO,EAEL,KAAK,SAAS,EACf,MAAM,iBAAiB,CAAC;AAczB,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAsBjD,OAAO,EAAyD,KAAK,eAAe,EAAE,MAAM,6BAA6B,CAAC;
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/chat/server.ts"],"names":[],"mappings":"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"}
|
package/dist/chat/server.js
CHANGED
|
@@ -58,6 +58,9 @@ import { getInstanceIdentity } from '../relay/instance_identity.js';
|
|
|
58
58
|
import { isMobilePaired, relayGate } from '../core/mobile_pairing_state.js';
|
|
59
59
|
import { relayPoll } from '../relay/relay_client.js';
|
|
60
60
|
import { startCommandConsumer } from '../relay/command_consumer.js';
|
|
61
|
+
import { postChatTurn } from '../relay/loopback_turn.js';
|
|
62
|
+
import { forgeErrorOf } from '../core/error_catalog.js';
|
|
63
|
+
import { formatForgeErrorText } from '../core/error_formatter.js';
|
|
61
64
|
import { getSessionStore, DEFAULT_SESSION_ID } from './session_store.js';
|
|
62
65
|
import { getSessionProject, setSessionProject } from './session_projects.js';
|
|
63
66
|
import { beginTurn, endTurn, interruptActiveTurn, interruptAllTurns, isTurnActive, activeTurnId, activeTurns, } from './turn_control.js';
|
|
@@ -1163,10 +1166,15 @@ function startDesktopCommandConsumer(port, coord = {}) {
|
|
|
1163
1166
|
void pub.flush(); /* push the reply promptly, do not wait the window */
|
|
1164
1167
|
};
|
|
1165
1168
|
const runTurn = async (text, sessionId, projectSlug) => {
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1169
|
+
/* El POST va por node:http y no por fetch, y con un presupuesto de turno
|
|
1170
|
+
* y no de llamada corta: las cabeceras de esta respuesta recien salen
|
|
1171
|
+
* cuando el turno TERMINO, y el limite que fetch hereda de undici son
|
|
1172
|
+
* cinco minutos medidos, asi que todo turno mas largo moria con "fetch
|
|
1173
|
+
* failed" aunque el escritorio lo terminara bien (PND-234). El porque
|
|
1174
|
+
* completo, con la medicion, esta en src/relay/loopback_turn.ts. */
|
|
1175
|
+
const resp = await postChatTurn({
|
|
1176
|
+
port,
|
|
1177
|
+
payload: {
|
|
1170
1178
|
messages: [{ role: 'user', content: text }],
|
|
1171
1179
|
...(sessionId ? { session_id: sessionId } : {}),
|
|
1172
1180
|
/* Bind the turn to the project the phone composed it against
|
|
@@ -1178,13 +1186,33 @@ function startDesktopCommandConsumer(port, coord = {}) {
|
|
|
1178
1186
|
* the unified-thread ones to the panel (the laptop sees what was
|
|
1179
1187
|
* asked from the phone). */
|
|
1180
1188
|
origin: 'mobile',
|
|
1181
|
-
}
|
|
1189
|
+
},
|
|
1182
1190
|
});
|
|
1183
|
-
|
|
1191
|
+
/* Un cuerpo que no es JSON no se puede reportar como "turn failed": eso
|
|
1192
|
+
* esconderia un 500 con una pagina de error adentro. Se dice que la
|
|
1193
|
+
* respuesta no se pudo leer y se deja el status, que es lo que permite
|
|
1194
|
+
* diagnosticarlo. */
|
|
1195
|
+
let j;
|
|
1196
|
+
try {
|
|
1197
|
+
j = JSON.parse(resp.body);
|
|
1198
|
+
}
|
|
1199
|
+
catch {
|
|
1200
|
+
throw new Error('respuesta no interpretable del servidor de chat (HTTP '
|
|
1201
|
+
+ resp.status + ')');
|
|
1202
|
+
}
|
|
1184
1203
|
if (!j || j.ok === false)
|
|
1185
1204
|
throw new Error(j?.error || 'turn failed');
|
|
1186
1205
|
return String(j.message?.text ?? '');
|
|
1187
1206
|
};
|
|
1207
|
+
/* Como se le cuenta al telefono un turno que fallo. El consumidor no
|
|
1208
|
+
* importa el catalogo de i18n a proposito (no toca el disco, para poder
|
|
1209
|
+
* probarse sin red ni servidor), asi que el renderizado se inyecta desde
|
|
1210
|
+
* aca, que ya lo tiene cargado. Devolver null significa "no tengo nada
|
|
1211
|
+
* mejor": el consumidor cae a su texto de siempre. */
|
|
1212
|
+
const describeError = (e) => {
|
|
1213
|
+
const forge = forgeErrorOf(e);
|
|
1214
|
+
return forge ? formatForgeErrorText(forge) : null;
|
|
1215
|
+
};
|
|
1188
1216
|
const onControl = async (action, args) => {
|
|
1189
1217
|
/* Control commands from the phone (tasks #33, #29) go through the
|
|
1190
1218
|
* same loopback the chat turns use -- no extra coupling. */
|
|
@@ -1313,7 +1341,7 @@ function startDesktopCommandConsumer(port, coord = {}) {
|
|
|
1313
1341
|
* slug names a peer's project (board.activeProject races between instances),
|
|
1314
1342
|
* so no instance ran it and the phone hung on "working". */
|
|
1315
1343
|
accept: (c) => !c.control && (!!c.targetInstance || ownsChatCommand(c.projectSlug)),
|
|
1316
|
-
pollCommands, runTurn, reply,
|
|
1344
|
+
pollCommands, runTurn, reply, describeError,
|
|
1317
1345
|
ackImmediate: true, /* confirm receipt before the turn runs */
|
|
1318
1346
|
onError: () => { },
|
|
1319
1347
|
});
|
|
@@ -1321,7 +1349,7 @@ function startDesktopCommandConsumer(port, coord = {}) {
|
|
|
1321
1349
|
enabled: relayEnabled, intervalMs: 1200,
|
|
1322
1350
|
instanceId: selfInstanceId,
|
|
1323
1351
|
accept: (c) => !!c.control,
|
|
1324
|
-
pollCommands, runTurn, onControl, reply,
|
|
1352
|
+
pollCommands, runTurn, onControl, reply, describeError,
|
|
1325
1353
|
onError: () => { },
|
|
1326
1354
|
});
|
|
1327
1355
|
/* Item 2 (cross-machine): advertise this instance's presence so the phone
|
|
@@ -1571,11 +1599,25 @@ async function handleForgeTaskDelete(req, res, ctx) {
|
|
|
1571
1599
|
* is thinking (Pablo 2026-09-02). Replaces the per-tool activity stream, which
|
|
1572
1600
|
* was the bulk of the relay's Cloudflare appends and only calmed anxiety. The
|
|
1573
1601
|
* pizarron (board) still refreshes -- THAT is the signal that adds value. */
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1602
|
+
/* UN latido POR TURNO, no uno por proceso (PND-237). Era un solo timer de
|
|
1603
|
+
* modulo y `startRelayHeartbeat()` arrancaba con un `stop()`, asi que con
|
|
1604
|
+
* turnos solapados -- que es lo normal en un panel -- el arranque de B mataba
|
|
1605
|
+
* el latido de A, y el cierre de A mataba el de B. Un turno sin latido deja de
|
|
1606
|
+
* dar senal de vida, y como la senal es lo unico que lo sostiene en el
|
|
1607
|
+
* registro, el anuncio quedaba a merced de cual timer sobrevivio. */
|
|
1608
|
+
const _relayHeartbeats = new Map();
|
|
1609
|
+
/** Para el latido de `id`; sin `id`, TODOS (apagado del servidor). */
|
|
1610
|
+
function stopRelayHeartbeat(id) {
|
|
1611
|
+
if (!id) {
|
|
1612
|
+
for (const t of _relayHeartbeats.values())
|
|
1613
|
+
clearInterval(t);
|
|
1614
|
+
_relayHeartbeats.clear();
|
|
1615
|
+
return;
|
|
1616
|
+
}
|
|
1617
|
+
const t = _relayHeartbeats.get(id);
|
|
1618
|
+
if (t) {
|
|
1619
|
+
clearInterval(t);
|
|
1620
|
+
_relayHeartbeats.delete(id);
|
|
1579
1621
|
}
|
|
1580
1622
|
}
|
|
1581
1623
|
/**
|
|
@@ -1608,19 +1650,28 @@ function turnoVivoSinAnunciar() {
|
|
|
1608
1650
|
return null;
|
|
1609
1651
|
}
|
|
1610
1652
|
}
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1653
|
+
/**
|
|
1654
|
+
* Arranca el latido DE ESTE turno. El id no es decorativo: es lo que ata la
|
|
1655
|
+
* senal de vida a quien la produce (PND-237). Sin turno identificado no hay
|
|
1656
|
+
* latido -- un pulso anonimo es justo el que mantenia vivo el anuncio de un
|
|
1657
|
+
* turno ajeno.
|
|
1658
|
+
*/
|
|
1659
|
+
function startRelayHeartbeat(turnId) {
|
|
1660
|
+
if (!turnId)
|
|
1661
|
+
return;
|
|
1662
|
+
stopRelayHeartbeat(turnId);
|
|
1663
|
+
const t = setInterval(() => {
|
|
1614
1664
|
/* SENAL DE VIDA del turno, antes que cualquier otra cosa. Este timer corre
|
|
1615
|
-
* SOLO entre turn_start y turn_complete, asi que su
|
|
1616
|
-
* que el turno sigue vivo -- y es lo que hace que un
|
|
1617
|
-
* siga anunciando. Va primero y fuera del try del
|
|
1618
|
-
* telefono pareado no significa que el turno este
|
|
1619
|
-
|
|
1665
|
+
* SOLO entre el turn_start y el turn_complete DE ESTE turno, asi que su
|
|
1666
|
+
* pulso ES la prueba de que el turno sigue vivo -- y es lo que hace que un
|
|
1667
|
+
* turno de tres horas se siga anunciando. Va primero y fuera del try del
|
|
1668
|
+
* publisher: que no haya telefono pareado no significa que el turno este
|
|
1669
|
+
* muerto. */
|
|
1670
|
+
touchInFlightTurn(turnId);
|
|
1620
1671
|
try {
|
|
1621
1672
|
const pub = getRelayPublisher();
|
|
1622
1673
|
if (!pub) {
|
|
1623
|
-
stopRelayHeartbeat();
|
|
1674
|
+
stopRelayHeartbeat(turnId);
|
|
1624
1675
|
return;
|
|
1625
1676
|
}
|
|
1626
1677
|
pub.activity('remote running...', { kind: 'heartbeat' });
|
|
@@ -1628,8 +1679,9 @@ function startRelayHeartbeat() {
|
|
|
1628
1679
|
}
|
|
1629
1680
|
catch { /* best-effort */ }
|
|
1630
1681
|
}, PULSO_DE_PROGRESO_MS);
|
|
1631
|
-
|
|
1632
|
-
|
|
1682
|
+
_relayHeartbeats.set(turnId, t);
|
|
1683
|
+
if (typeof t.unref === 'function')
|
|
1684
|
+
t.unref();
|
|
1633
1685
|
}
|
|
1634
1686
|
function relayForward(event, data) {
|
|
1635
1687
|
const pub = getRelayPublisher();
|
|
@@ -1637,19 +1689,24 @@ function relayForward(event, data) {
|
|
|
1637
1689
|
return;
|
|
1638
1690
|
try {
|
|
1639
1691
|
const d = (data && typeof data === 'object') ? data : {};
|
|
1692
|
+
/* El id del turno viaja en el evento (PND-237): el latido y el registro de
|
|
1693
|
+
* turnos vivos son POR TURNO, asi que hay que saber de cual se habla. Un
|
|
1694
|
+
* evento sin id no toca ningun timer -- antes que matar el latido de otro
|
|
1695
|
+
* turno, preferimos dejar que este venza por silencio. */
|
|
1696
|
+
const tid = typeof d.turn_id === 'string' ? d.turn_id : '';
|
|
1640
1697
|
/* tool_round_start: intentionally NO relay activity anymore -- the granular
|
|
1641
1698
|
* per-tool feed was the append-heavy part; the phone now gets a heartbeat. */
|
|
1642
1699
|
if (event === 'turn_start') {
|
|
1643
1700
|
pub.status('working', { tier: d.tier, provider: d.provider, model: d.model });
|
|
1644
1701
|
pub.activity('remote running...', { kind: 'heartbeat' }); /* first line now; then every 60s */
|
|
1645
|
-
startRelayHeartbeat();
|
|
1702
|
+
startRelayHeartbeat(tid);
|
|
1646
1703
|
/* El turno en vuelo viaja DENTRO del latido (in_flight_turn.ts): hay que
|
|
1647
1704
|
* publicarlo ya, sin esperar el tick, o el telefono no sabe que hay algo
|
|
1648
1705
|
* corriendo hasta la proxima evaluacion. */
|
|
1649
1706
|
forcePresenceBeat();
|
|
1650
1707
|
}
|
|
1651
1708
|
else if (event === 'turn_complete') {
|
|
1652
|
-
stopRelayHeartbeat();
|
|
1709
|
+
stopRelayHeartbeat(tid);
|
|
1653
1710
|
pub.status('round_done');
|
|
1654
1711
|
/* Simetrico: el latido tiene que dejar de anunciar el turno, o el
|
|
1655
1712
|
* telefono sigue mostrando la burbuja "pensando" hasta el proximo. */
|
|
@@ -1659,7 +1716,7 @@ function relayForward(event, data) {
|
|
|
1659
1716
|
publishBoardSnapshot();
|
|
1660
1717
|
}
|
|
1661
1718
|
else if (event === 'error') {
|
|
1662
|
-
stopRelayHeartbeat();
|
|
1719
|
+
stopRelayHeartbeat(tid);
|
|
1663
1720
|
pub.status('error', { message: d.message ?? d.error ?? '' });
|
|
1664
1721
|
}
|
|
1665
1722
|
}
|
|
@@ -6119,6 +6176,11 @@ async function handleChat(req, res, ctx) {
|
|
|
6119
6176
|
* del turno en vuelo -- y lo que se declara dentro del try no se ve desde
|
|
6120
6177
|
* ahi. Mismo motivo por el que turnId vive en este nivel. */
|
|
6121
6178
|
let turnEcho = null;
|
|
6179
|
+
/* El id del turno para el LATIDO, en este nivel por el mismo motivo que
|
|
6180
|
+
* turnEcho: el finally tiene que poder apagarlo y `_xTurnId` se declara
|
|
6181
|
+
* dentro del try. Y no alcanza con turnEcho: ese se saltea los turnos de
|
|
6182
|
+
* origen mobile, pero el latido se arranca para TODOS (PND-237). */
|
|
6183
|
+
let latidoTurnId = null;
|
|
6122
6184
|
/* alpha.59e -- SSE streaming mode. Client opts in via
|
|
6123
6185
|
* body.stream=true. We keep the response open + emit events
|
|
6124
6186
|
* during the tool loop instead of bundling everything in a
|
|
@@ -6294,6 +6356,7 @@ async function handleChat(req, res, ctx) {
|
|
|
6294
6356
|
* the END remoteTurn (same id) fills the reply. Old phones ignore
|
|
6295
6357
|
* the extra fields and keep their current end-of-turn behavior. */
|
|
6296
6358
|
const _xTurnId = (body.origin === 'mobile' ? 'mb-' : body.origin === 'bridge' ? 'br-' : 'dt-') + Date.now().toString(36);
|
|
6359
|
+
latidoTurnId = _xTurnId;
|
|
6297
6360
|
/* Queda registrado como ESTADO para que el latido de presencia lo
|
|
6298
6361
|
* repita: ver el comentario largo en RelayPublisher.presence().
|
|
6299
6362
|
*
|
|
@@ -6460,6 +6523,11 @@ async function handleChat(req, res, ctx) {
|
|
|
6460
6523
|
/* PND-132 -- so the relay activity feed shows what is being processed
|
|
6461
6524
|
* (a per-turn heartbeat), not just tool rounds. */
|
|
6462
6525
|
user_text: lastUserText.slice(0, 80),
|
|
6526
|
+
/* PND-237 -- QUIEN arranca. El latido del relay y el registro de turnos
|
|
6527
|
+
* vivos son por turno; sin el id, relayForward no sabe a cual atar el
|
|
6528
|
+
* pulso y el panel vuelve a tener un unico timer que los turnos
|
|
6529
|
+
* solapados se pisan entre si. */
|
|
6530
|
+
turn_id: _xTurnId,
|
|
6463
6531
|
});
|
|
6464
6532
|
}
|
|
6465
6533
|
/* alpha.59e -- loop detection counter. We track (verb_id +
|
|
@@ -7446,6 +7514,10 @@ async function handleChat(req, res, ctx) {
|
|
|
7446
7514
|
const responsePayload = {
|
|
7447
7515
|
ok: true,
|
|
7448
7516
|
message: { role: 'assistant', text: reply.text },
|
|
7517
|
+
/* PND-237 -- QUIEN termina. Simetrico al turn_start: relayForward para el
|
|
7518
|
+
* latido DE ESTE turno y no el del turno de al lado, que es lo que pasaba
|
|
7519
|
+
* con un unico timer de modulo. */
|
|
7520
|
+
turn_id: _xTurnId,
|
|
7449
7521
|
/* Interrupt (task #27): tell the panel the turn was stopped
|
|
7450
7522
|
* mid-flight so it can render the partial answer with a
|
|
7451
7523
|
* "(interrumpido)" marker instead of treating it as complete. */
|
|
@@ -7604,6 +7676,15 @@ async function handleChat(req, res, ctx) {
|
|
|
7604
7676
|
* de un turno que ya arranco despues. */
|
|
7605
7677
|
if (turnEcho)
|
|
7606
7678
|
clearInFlightTurn(turnEcho.id);
|
|
7679
|
+
/* Y el LATIDO de este turno, por el mismo motivo y en el mismo lugar
|
|
7680
|
+
* (PND-237). Va aca y no en cada rama de error -- son cinco emisores de
|
|
7681
|
+
* `error` y la quinta se iba a olvidar -- porque el finally es el unico
|
|
7682
|
+
* punto por el que pasan TODAS las salidas. Un latido que sobrevive a su
|
|
7683
|
+
* turno sigue dando senal de vida por el, y entonces el tope de silencio
|
|
7684
|
+
* nunca lo saca del registro: es la mitad que convierte un turno colgado
|
|
7685
|
+
* en un turno que se anuncia para siempre. */
|
|
7686
|
+
if (latidoTurnId)
|
|
7687
|
+
stopRelayHeartbeat(latidoTurnId);
|
|
7607
7688
|
/* Interrupt (task #27): always clear this turn from the active
|
|
7608
7689
|
* registry, no matter how the turn exits. endTurn is a no-op if a
|
|
7609
7690
|
* newer turn has already replaced us, so it never clobbers a
|