@nac3/forge-cli 1.21.4 → 1.23.1
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/bridge.d.ts +43 -1
- package/dist/chat/bridge.d.ts.map +1 -1
- package/dist/chat/bridge.js +176 -31
- package/dist/chat/bridge.js.map +1 -1
- package/dist/chat/bridge_inbox.d.ts +143 -0
- package/dist/chat/bridge_inbox.d.ts.map +1 -0
- package/dist/chat/bridge_inbox.js +219 -0
- package/dist/chat/bridge_inbox.js.map +1 -0
- package/dist/chat/claude.d.ts +10 -0
- package/dist/chat/claude.d.ts.map +1 -1
- package/dist/chat/claude.js.map +1 -1
- package/dist/chat/panel.d.ts.map +1 -1
- package/dist/chat/panel.js +142 -15
- package/dist/chat/panel.js.map +1 -1
- package/dist/chat/recent_sessions_recap.d.ts.map +1 -1
- package/dist/chat/recent_sessions_recap.js +18 -6
- package/dist/chat/recent_sessions_recap.js.map +1 -1
- package/dist/chat/server.d.ts +1 -1
- package/dist/chat/server.d.ts.map +1 -1
- package/dist/chat/server.js +346 -53
- package/dist/chat/server.js.map +1 -1
- package/dist/chat/tools/bridge_verbs.d.ts +10 -0
- package/dist/chat/tools/bridge_verbs.d.ts.map +1 -1
- package/dist/chat/tools/bridge_verbs.js +76 -3
- package/dist/chat/tools/bridge_verbs.js.map +1 -1
- package/dist/chat/tools/nac3_invoke.d.ts.map +1 -1
- package/dist/chat/tools/nac3_invoke.js +20 -3
- package/dist/chat/tools/nac3_invoke.js.map +1 -1
- package/dist/chat/tools/pending.d.ts.map +1 -1
- package/dist/chat/tools/pending.js +19 -2
- package/dist/chat/tools/pending.js.map +1 -1
- package/dist/chat/tools.d.ts +6 -0
- package/dist/chat/tools.d.ts.map +1 -1
- package/dist/chat/tools.js +4 -0
- package/dist/chat/tools.js.map +1 -1
- package/dist/chat/turn_control.d.ts +2 -0
- package/dist/chat/turn_control.d.ts.map +1 -1
- package/dist/chat/turn_control.js +30 -1
- package/dist/chat/turn_control.js.map +1 -1
- package/dist/commands/urazuke.d.ts.map +1 -1
- package/dist/commands/urazuke.js +68 -6
- package/dist/commands/urazuke.js.map +1 -1
- package/dist/core/cost_router.d.ts +7 -0
- package/dist/core/cost_router.d.ts.map +1 -1
- package/dist/core/cost_router.js +8 -5
- package/dist/core/cost_router.js.map +1 -1
- package/dist/core/limits_config.d.ts +37 -6
- package/dist/core/limits_config.d.ts.map +1 -1
- package/dist/core/limits_config.js +74 -8
- package/dist/core/limits_config.js.map +1 -1
- package/dist/nac3/danger_zone.d.ts +39 -0
- package/dist/nac3/danger_zone.d.ts.map +1 -0
- package/dist/nac3/danger_zone.js +31 -0
- package/dist/nac3/danger_zone.js.map +1 -0
- package/dist/nac3/internal_manifest.d.ts.map +1 -1
- package/dist/nac3/internal_manifest.js +33 -0
- package/dist/nac3/internal_manifest.js.map +1 -1
- package/dist/pending/store.d.ts +7 -0
- package/dist/pending/store.d.ts.map +1 -1
- package/dist/pending/store.js +41 -0
- package/dist/pending/store.js.map +1 -1
- package/dist/urazuke/catalog.d.ts +1 -1
- package/dist/urazuke/catalog.d.ts.map +1 -1
- package/dist/urazuke/catalog.js.map +1 -1
- package/dist/urazuke/gate.d.ts.map +1 -1
- package/dist/urazuke/gate.js +98 -6
- package/dist/urazuke/gate.js.map +1 -1
- package/dist/urazuke/gates_data.d.ts +8 -1
- package/dist/urazuke/gates_data.d.ts.map +1 -1
- package/dist/urazuke/gates_data.js +25 -3
- package/dist/urazuke/gates_data.js.map +1 -1
- package/dist/urazuke/heuristics.d.ts +108 -0
- package/dist/urazuke/heuristics.d.ts.map +1 -0
- package/dist/urazuke/heuristics.js +248 -0
- package/dist/urazuke/heuristics.js.map +1 -0
- package/dist/urazuke/manual_cases.d.ts +31 -0
- package/dist/urazuke/manual_cases.d.ts.map +1 -0
- package/dist/urazuke/manual_cases.js +87 -0
- package/dist/urazuke/manual_cases.js.map +1 -0
- package/dist/urazuke/project_config.d.ts +25 -0
- package/dist/urazuke/project_config.d.ts.map +1 -0
- package/dist/urazuke/project_config.js +65 -0
- package/dist/urazuke/project_config.js.map +1 -0
- package/dist/urazuke/quality.d.ts +0 -8
- package/dist/urazuke/quality.d.ts.map +1 -1
- package/dist/urazuke/quality.js +4 -29
- package/dist/urazuke/quality.js.map +1 -1
- package/dist/urazuke/scoreboard.d.ts +85 -0
- package/dist/urazuke/scoreboard.d.ts.map +1 -0
- package/dist/urazuke/scoreboard.js +260 -0
- package/dist/urazuke/scoreboard.js.map +1 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/workflow/orchestrator.d.ts +7 -3
- package/dist/workflow/orchestrator.d.ts.map +1 -1
- package/dist/workflow/orchestrator.js +1 -1
- package/dist/workflow/orchestrator.js.map +1 -1
- package/package.json +1 -1
- package/src/i18n/catalogs/ar.json +558 -550
- package/src/i18n/catalogs/de.json +558 -550
- package/src/i18n/catalogs/en.json +558 -550
- package/src/i18n/catalogs/es.json +558 -550
- package/src/i18n/catalogs/fr.json +558 -550
- package/src/i18n/catalogs/hi.json +558 -550
- package/src/i18n/catalogs/it.json +558 -550
- package/src/i18n/catalogs/ja.json +558 -550
- package/src/i18n/catalogs/pt.json +558 -550
- package/src/i18n/catalogs/zh.json +558 -550
package/dist/chat/server.js
CHANGED
|
@@ -784,6 +784,28 @@ export async function startChatServer(opts) {
|
|
|
784
784
|
catch { /* swallow */ }
|
|
785
785
|
}
|
|
786
786
|
}
|
|
787
|
+
/* PND-222 -- el drenaje de la casilla del canal se engancha al CIERRE DE
|
|
788
|
+
* TURNO, no a un temporizador. Es el unico momento en que un entrante puede
|
|
789
|
+
* correr sin pelearse con el turno vivo, y el unico que lo conoce es
|
|
790
|
+
* turn_control. Registrado una sola vez por proceso. */
|
|
791
|
+
if (!bridgeDisabled) {
|
|
792
|
+
try {
|
|
793
|
+
const { onTurnsIdle } = await import('./turn_control.js');
|
|
794
|
+
onTurnsIdle(() => {
|
|
795
|
+
void drainBridgeInbox({
|
|
796
|
+
projectRoot: session.projectRoot,
|
|
797
|
+
projectName: session.projectName,
|
|
798
|
+
projectSlug: session.projectSlug,
|
|
799
|
+
port: opts.port,
|
|
800
|
+
claude,
|
|
801
|
+
voice,
|
|
802
|
+
store,
|
|
803
|
+
bridge: bridgeHandle,
|
|
804
|
+
});
|
|
805
|
+
});
|
|
806
|
+
}
|
|
807
|
+
catch { /* el canal nunca puede trabar el boot */ }
|
|
808
|
+
}
|
|
787
809
|
/* Set the relay board ctx to the active project NOW so a paired phone
|
|
788
810
|
* shows the project bubble + pizarron immediately -- the ctx used to be
|
|
789
811
|
* set only on a chat turn or a project switch, so the phone saw nothing
|
|
@@ -884,6 +906,122 @@ export async function startChatServer(opts) {
|
|
|
884
906
|
}),
|
|
885
907
|
};
|
|
886
908
|
}
|
|
909
|
+
/** Entrantes esperando el modal del dueno (modo 'ask'), por delivery_id. */
|
|
910
|
+
const bridgeAsks = new Map();
|
|
911
|
+
/** delivery_id local -> delivery_id del expediente en el host del canal, para
|
|
912
|
+
* poder devolverle la transicion al emisor. */
|
|
913
|
+
const bridgeHostDeliveryIds = new Map();
|
|
914
|
+
function hostIdFor(localId) {
|
|
915
|
+
return bridgeHostDeliveryIds.get(localId) || localId;
|
|
916
|
+
}
|
|
917
|
+
/** Lanza el turno inyectado por el MISMO camino que un turno tipeado. */
|
|
918
|
+
function runInjectedTurn(ctx, turn, localId) {
|
|
919
|
+
const hostId = hostIdFor(localId);
|
|
920
|
+
void (async () => {
|
|
921
|
+
try {
|
|
922
|
+
await ctx.bridge?.reportDelivery(hostId, 'running');
|
|
923
|
+
}
|
|
924
|
+
catch { /* */ }
|
|
925
|
+
try {
|
|
926
|
+
await fetch('http://127.0.0.1:' + ctx.port + '/api/chat', {
|
|
927
|
+
method: 'POST',
|
|
928
|
+
headers: { 'content-type': 'application/json' },
|
|
929
|
+
body: JSON.stringify(turn),
|
|
930
|
+
});
|
|
931
|
+
try {
|
|
932
|
+
await ctx.bridge?.reportDelivery(hostId, 'done');
|
|
933
|
+
}
|
|
934
|
+
catch { /* */ }
|
|
935
|
+
}
|
|
936
|
+
catch {
|
|
937
|
+
try {
|
|
938
|
+
await ctx.bridge?.reportDelivery(hostId, 'refused', 'local turn failed to start');
|
|
939
|
+
}
|
|
940
|
+
catch { /* */ }
|
|
941
|
+
}
|
|
942
|
+
})();
|
|
943
|
+
}
|
|
944
|
+
let draining = false;
|
|
945
|
+
/**
|
|
946
|
+
* Tomar el proximo entrante y hacer lo que la postura del dueno diga. Se llama
|
|
947
|
+
* al recibir (si no hay turno) y al cerrar cada turno.
|
|
948
|
+
*/
|
|
949
|
+
async function drainBridgeInbox(ctx) {
|
|
950
|
+
if (draining)
|
|
951
|
+
return;
|
|
952
|
+
draining = true;
|
|
953
|
+
try {
|
|
954
|
+
const { isTurnActive } = await import('./turn_control.js');
|
|
955
|
+
if (isTurnActive())
|
|
956
|
+
return;
|
|
957
|
+
if (bridgeAsks.size > 0)
|
|
958
|
+
return; /* un modal abierto manda; no apilamos dos */
|
|
959
|
+
const inbox = await import('./bridge_inbox.js');
|
|
960
|
+
/* Lo vencido se DICE, no se descarta: el emisor lo lee en su expediente. */
|
|
961
|
+
for (const v of inbox.dropExpired()) {
|
|
962
|
+
await inbox.logBridgeDelivery({
|
|
963
|
+
ts: new Date().toISOString(), delivery_id: hostIdFor(v.delivery_id), direction: 'in',
|
|
964
|
+
from: v.from_name, from_port: v.from_port, hop: v.hop, disposition: 'expired',
|
|
965
|
+
note: 'espero mas de ' + Math.round(inbox.INBOX_TTL_MS / 60000) + ' min sin poder correr',
|
|
966
|
+
text_len: v.text.length,
|
|
967
|
+
});
|
|
968
|
+
try {
|
|
969
|
+
await ctx.bridge?.reportDelivery(hostIdFor(v.delivery_id), 'expired', 'ttl');
|
|
970
|
+
}
|
|
971
|
+
catch { /* */ }
|
|
972
|
+
}
|
|
973
|
+
const msg = inbox.takeNextDirected();
|
|
974
|
+
if (!msg)
|
|
975
|
+
return;
|
|
976
|
+
const { readLimitsConfig } = await import('../core/limits_config.js');
|
|
977
|
+
const cfg = (await readLimitsConfig()).bridge;
|
|
978
|
+
const plan = inbox.planDelivery(msg, cfg);
|
|
979
|
+
if (plan.action === 'refuse') {
|
|
980
|
+
try {
|
|
981
|
+
await ctx.bridge?.reportDelivery(hostIdFor(msg.delivery_id), 'refused', 'disabled');
|
|
982
|
+
}
|
|
983
|
+
catch { /* */ }
|
|
984
|
+
return;
|
|
985
|
+
}
|
|
986
|
+
if (plan.action === 'ask') {
|
|
987
|
+
bridgeAsks.set(msg.delivery_id, msg);
|
|
988
|
+
/* EL MODAL ES LA PREGUNTA (R8): emisor, puerto, salto y el texto
|
|
989
|
+
* LITERAL, sin recortarlo a un resumen. */
|
|
990
|
+
broadcastToPanel('approval_required', {
|
|
991
|
+
kind: 'bridge_message',
|
|
992
|
+
trace_id: msg.delivery_id,
|
|
993
|
+
bridge: {
|
|
994
|
+
delivery_id: msg.delivery_id,
|
|
995
|
+
from_name: msg.from_name,
|
|
996
|
+
from_port: msg.from_port,
|
|
997
|
+
hop: msg.hop,
|
|
998
|
+
text: msg.text,
|
|
999
|
+
received_at: new Date(msg.received_at).toISOString(),
|
|
1000
|
+
},
|
|
1001
|
+
});
|
|
1002
|
+
await inbox.logBridgeDelivery({
|
|
1003
|
+
ts: new Date().toISOString(), delivery_id: hostIdFor(msg.delivery_id), direction: 'in',
|
|
1004
|
+
from: msg.from_name, from_port: msg.from_port, hop: msg.hop,
|
|
1005
|
+
disposition: 'awaiting_approval', mode: cfg.directed_mode, text_len: msg.text.length,
|
|
1006
|
+
});
|
|
1007
|
+
try {
|
|
1008
|
+
await ctx.bridge?.reportDelivery(hostIdFor(msg.delivery_id), 'awaiting_approval');
|
|
1009
|
+
}
|
|
1010
|
+
catch { /* */ }
|
|
1011
|
+
return;
|
|
1012
|
+
}
|
|
1013
|
+
await inbox.logBridgeDelivery({
|
|
1014
|
+
ts: new Date().toISOString(), delivery_id: hostIdFor(msg.delivery_id), direction: 'in',
|
|
1015
|
+
from: msg.from_name, from_port: msg.from_port, hop: msg.hop,
|
|
1016
|
+
disposition: 'running', mode: cfg.directed_mode, text_len: msg.text.length,
|
|
1017
|
+
});
|
|
1018
|
+
runInjectedTurn(ctx, plan.turn, msg.delivery_id);
|
|
1019
|
+
}
|
|
1020
|
+
catch { /* el drenaje nunca puede tirar el servidor */ }
|
|
1021
|
+
finally {
|
|
1022
|
+
draining = false;
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
887
1025
|
/* P2 (mobile remote control) -- the desktop mirrors its Coding-feed
|
|
888
1026
|
* activity to the worker relay so a paired phone can watch the work.
|
|
889
1027
|
* AUTOMATIC: on as soon as a phone is paired (the pairing drops a marker);
|
|
@@ -1975,7 +2113,13 @@ async function route(req, res, ctx) {
|
|
|
1975
2113
|
};
|
|
1976
2114
|
const fields = LIMITS_FIELD_PATHS.map((f) => resolveField(f, 'number'));
|
|
1977
2115
|
const booleans = LIMITS_BOOLEAN_FIELD_PATHS.map((f) => resolveField(f, 'boolean'));
|
|
1978
|
-
|
|
2116
|
+
/* PND-221 -- los ajustes de tres estados. El panel pinta el select desde
|
|
2117
|
+
* esta lista; los valores validos salen del mismo lugar que los valida. */
|
|
2118
|
+
const { LIMITS_ENUM_FIELD_PATHS } = await import('../core/limits_config.js');
|
|
2119
|
+
const enums = LIMITS_ENUM_FIELD_PATHS.map((e) => ({
|
|
2120
|
+
...resolveField(e.field, 'boolean'), kind: 'enum', values: e.values,
|
|
2121
|
+
}));
|
|
2122
|
+
sendJson(res, 200, { ok: true, fields, booleans, enums, config: cfg });
|
|
1979
2123
|
return;
|
|
1980
2124
|
}
|
|
1981
2125
|
if (req.method === 'POST' && url.pathname === '/api/forge/limits') {
|
|
@@ -1988,7 +2132,7 @@ async function route(req, res, ctx) {
|
|
|
1988
2132
|
sendJson(res, 400, { ok: false, error: 'invalid JSON' });
|
|
1989
2133
|
return;
|
|
1990
2134
|
}
|
|
1991
|
-
const { setLimitsField, setLimitsBooleanField, isLimitsBooleanFieldPath, resetLimitsConfig } = await import('../core/limits_config.js');
|
|
2135
|
+
const { setLimitsField, setLimitsBooleanField, isLimitsBooleanFieldPath, resetLimitsConfig, setLimitsEnumField, isLimitsEnumFieldPath, } = await import('../core/limits_config.js');
|
|
1992
2136
|
if (body.reset === true) {
|
|
1993
2137
|
const cfg = await resetLimitsConfig();
|
|
1994
2138
|
sendJson(res, 200, { ok: true, config: cfg });
|
|
@@ -1998,6 +2142,21 @@ async function route(req, res, ctx) {
|
|
|
1998
2142
|
sendJson(res, 400, { ok: false, error: 'expected {field: string, value: number|boolean} or {reset: true}' });
|
|
1999
2143
|
return;
|
|
2000
2144
|
}
|
|
2145
|
+
/* PND-221 -- campos de enumeracion (bridge.directed_mode, bridge.tier). */
|
|
2146
|
+
if (isLimitsEnumFieldPath(body.field)) {
|
|
2147
|
+
if (typeof body.value !== 'string') {
|
|
2148
|
+
sendJson(res, 400, { ok: false, error: 'enum field requires a string value' });
|
|
2149
|
+
return;
|
|
2150
|
+
}
|
|
2151
|
+
try {
|
|
2152
|
+
const cfg = await setLimitsEnumField(body.field, body.value);
|
|
2153
|
+
sendJson(res, 200, { ok: true, config: cfg });
|
|
2154
|
+
}
|
|
2155
|
+
catch (err) {
|
|
2156
|
+
sendJson(res, 400, { ok: false, error: err instanceof Error ? err.message : String(err) });
|
|
2157
|
+
}
|
|
2158
|
+
return;
|
|
2159
|
+
}
|
|
2001
2160
|
/* alpha.59d -- accept booleans too (Danger Zone toggle +
|
|
2002
2161
|
* cwd_boundary_strict). Dispatch by field-path type. */
|
|
2003
2162
|
if (isLimitsBooleanFieldPath(body.field)) {
|
|
@@ -2239,11 +2398,20 @@ async function route(req, res, ctx) {
|
|
|
2239
2398
|
}
|
|
2240
2399
|
return;
|
|
2241
2400
|
}
|
|
2242
|
-
/* Inbox for DIRECTED bridge messages (Pablo 2026-06-19
|
|
2243
|
-
*
|
|
2244
|
-
*
|
|
2245
|
-
*
|
|
2246
|
-
*
|
|
2401
|
+
/* Inbox for DIRECTED bridge messages (Pablo 2026-06-19; reescrito 2026-10-27
|
|
2402
|
+
* por PND-221..227). El bridge host POSTea aca (solo localhost) para empujar
|
|
2403
|
+
* un mensaje dirigido a esta instancia.
|
|
2404
|
+
*
|
|
2405
|
+
* TRES COSAS CAMBIARON, y las tres venian de la misma raiz -- el canal
|
|
2406
|
+
* trataba distinto al mensaje de otra instancia que al que el dueno tipea:
|
|
2407
|
+
*
|
|
2408
|
+
* 1. Ya NO contesta 'busy'. Siempre acepta y ENCOLA (bridge_inbox.ts); el
|
|
2409
|
+
* drenaje lo dispara el cierre del turno. La ventana de doce segundos
|
|
2410
|
+
* que perdio seis de diez mensajes el 27-oct desaparece.
|
|
2411
|
+
* 2. Ya NO fuerza 'eco' ni reescribe el prompt. El texto del emisor llega
|
|
2412
|
+
* caracter por caracter y la procedencia viaja en metadatos.
|
|
2413
|
+
* 3. El interruptor dejo de ser binario: off / ask / auto, y 'ask' abre el
|
|
2414
|
+
* modal con el texto literal. */
|
|
2247
2415
|
if (req.method === 'POST' && url.pathname === '/api/forge/bridge/inbox') {
|
|
2248
2416
|
const raw = await readBody(req);
|
|
2249
2417
|
let body;
|
|
@@ -2259,57 +2427,121 @@ async function route(req, res, ctx) {
|
|
|
2259
2427
|
sendJson(res, 400, { ok: false, accepted: false, reason: 'empty' });
|
|
2260
2428
|
return;
|
|
2261
2429
|
}
|
|
2262
|
-
|
|
2430
|
+
const { enqueueDirected, logBridgeDelivery, senderPort } = await import('./bridge_inbox.js');
|
|
2431
|
+
const fromName = String(body.from_name || 'otra instancia');
|
|
2432
|
+
const fromId = String(body.from || '');
|
|
2433
|
+
const hop = Math.max(0, Math.min(10, Number(body.hop) || 0));
|
|
2434
|
+
const hostDeliveryId = body.delivery_id ? String(body.delivery_id) : '';
|
|
2435
|
+
let mode = 'ask';
|
|
2263
2436
|
try {
|
|
2264
2437
|
const { readLimitsConfig } = await import('../core/limits_config.js');
|
|
2265
|
-
|
|
2438
|
+
mode = (await readLimitsConfig()).bridge.directed_mode;
|
|
2266
2439
|
}
|
|
2267
2440
|
catch {
|
|
2268
|
-
|
|
2441
|
+
mode = 'ask';
|
|
2269
2442
|
}
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2443
|
+
if (mode === 'off') {
|
|
2444
|
+
await logBridgeDelivery({
|
|
2445
|
+
ts: new Date().toISOString(), delivery_id: hostDeliveryId || '-', direction: 'in',
|
|
2446
|
+
from: fromName, from_port: senderPort(fromId), hop, disposition: 'refused',
|
|
2447
|
+
mode, note: 'directed_mode=off', text_len: text.length,
|
|
2448
|
+
});
|
|
2276
2449
|
sendJson(res, 200, { ok: true, accepted: false, reason: 'disabled' });
|
|
2277
2450
|
return;
|
|
2278
2451
|
}
|
|
2279
|
-
|
|
2452
|
+
const q = enqueueDirected({ from: fromId, from_name: fromName, text, hop });
|
|
2453
|
+
if (hostDeliveryId)
|
|
2454
|
+
bridgeHostDeliveryIds.set(q.delivery_id, hostDeliveryId);
|
|
2455
|
+
await logBridgeDelivery({
|
|
2456
|
+
ts: new Date().toISOString(), delivery_id: hostDeliveryId || '-', direction: 'in',
|
|
2457
|
+
from: fromName, from_port: senderPort(fromId), hop,
|
|
2458
|
+
disposition: q.duplicate_of ? 'duplicate' : 'queued',
|
|
2459
|
+
mode, note: q.evicted ? 'evicted ' + q.evicted : undefined, text_len: text.length,
|
|
2460
|
+
});
|
|
2461
|
+
sendJson(res, 200, {
|
|
2462
|
+
ok: true, accepted: true, queued: true, position: q.position,
|
|
2463
|
+
status: 'queued', delivery_id: hostDeliveryId || q.delivery_id,
|
|
2464
|
+
...(q.duplicate_of ? { duplicate_of: q.duplicate_of } : {}),
|
|
2465
|
+
});
|
|
2466
|
+
/* Si no hay turno vivo, drenar YA; si hay, lo drena onTurnsIdle. */
|
|
2467
|
+
void drainBridgeInbox(ctx);
|
|
2468
|
+
return;
|
|
2469
|
+
}
|
|
2470
|
+
/* El dueno resolvio el modal de un entrante (modo 'ask'). trust=true suma el
|
|
2471
|
+
* par al allowlist para que el proximo del MISMO emisor no pregunte. */
|
|
2472
|
+
if (req.method === 'POST' && url.pathname === '/api/forge/bridge/approve') {
|
|
2473
|
+
const raw = await readBody(req);
|
|
2474
|
+
let body;
|
|
2475
|
+
try {
|
|
2476
|
+
body = JSON.parse(raw);
|
|
2477
|
+
}
|
|
2478
|
+
catch {
|
|
2479
|
+
sendJson(res, 400, { ok: false, error: 'invalid JSON' });
|
|
2480
|
+
return;
|
|
2481
|
+
}
|
|
2482
|
+
const id = String(body.delivery_id || '');
|
|
2483
|
+
const row = bridgeAsks.get(id);
|
|
2484
|
+
if (!row) {
|
|
2485
|
+
sendJson(res, 404, { ok: false, error: 'no pending bridge message with that id' });
|
|
2486
|
+
return;
|
|
2487
|
+
}
|
|
2488
|
+
bridgeAsks.delete(id);
|
|
2489
|
+
if (body.trust === true) {
|
|
2280
2490
|
try {
|
|
2281
|
-
|
|
2491
|
+
const { setBridgeAutoFrom } = await import('../core/limits_config.js');
|
|
2492
|
+
const { senderIdentity } = await import('./bridge_inbox.js');
|
|
2493
|
+
await setBridgeAutoFrom(senderIdentity(row.from, row.from_name), true);
|
|
2282
2494
|
}
|
|
2283
|
-
catch { /* */ }
|
|
2284
|
-
|
|
2495
|
+
catch { /* la confianza es un extra; la ejecucion no depende de ella */ }
|
|
2496
|
+
}
|
|
2497
|
+
const { logBridgeDelivery, composeInjectedTurn } = await import('./bridge_inbox.js');
|
|
2498
|
+
let tier = 'default';
|
|
2499
|
+
try {
|
|
2500
|
+
const { readLimitsConfig } = await import('../core/limits_config.js');
|
|
2501
|
+
tier = (await readLimitsConfig()).bridge.tier;
|
|
2502
|
+
}
|
|
2503
|
+
catch { /* default */ }
|
|
2504
|
+
await logBridgeDelivery({
|
|
2505
|
+
ts: new Date().toISOString(), delivery_id: id, direction: 'in',
|
|
2506
|
+
from: row.from_name, from_port: row.from_port, hop: row.hop,
|
|
2507
|
+
disposition: 'running', mode: 'ask', approved_by: 'owner (panel modal)',
|
|
2508
|
+
text_len: row.text.length,
|
|
2509
|
+
});
|
|
2510
|
+
runInjectedTurn(ctx, composeInjectedTurn(row, tier), id);
|
|
2511
|
+
sendJson(res, 200, { ok: true, running: true });
|
|
2512
|
+
return;
|
|
2513
|
+
}
|
|
2514
|
+
if (req.method === 'POST' && url.pathname === '/api/forge/bridge/reject') {
|
|
2515
|
+
const raw = await readBody(req);
|
|
2516
|
+
let body;
|
|
2517
|
+
try {
|
|
2518
|
+
body = JSON.parse(raw);
|
|
2519
|
+
}
|
|
2520
|
+
catch {
|
|
2521
|
+
sendJson(res, 400, { ok: false, error: 'invalid JSON' });
|
|
2285
2522
|
return;
|
|
2286
2523
|
}
|
|
2287
|
-
const
|
|
2288
|
-
const
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2524
|
+
const id = String(body.delivery_id || '');
|
|
2525
|
+
const row = bridgeAsks.get(id);
|
|
2526
|
+
if (!row) {
|
|
2527
|
+
sendJson(res, 404, { ok: false, error: 'no pending bridge message with that id' });
|
|
2528
|
+
return;
|
|
2529
|
+
}
|
|
2530
|
+
bridgeAsks.delete(id);
|
|
2531
|
+
const { logBridgeDelivery } = await import('./bridge_inbox.js');
|
|
2532
|
+
await logBridgeDelivery({
|
|
2533
|
+
ts: new Date().toISOString(), delivery_id: id, direction: 'in',
|
|
2534
|
+
from: row.from_name, from_port: row.from_port, hop: row.hop,
|
|
2535
|
+
disposition: 'refused', mode: 'ask', approved_by: 'owner (panel modal)',
|
|
2536
|
+
note: body.reason ? String(body.reason).slice(0, 200) : 'rejected in modal',
|
|
2537
|
+
text_len: row.text.length,
|
|
2538
|
+
});
|
|
2292
2539
|
try {
|
|
2293
|
-
|
|
2540
|
+
await ctx.bridge?.reportDelivery(hostIdFor(id), 'refused', 'rejected by owner');
|
|
2294
2541
|
}
|
|
2295
|
-
catch { /* */ }
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
+ '\n\n(Responde en el chat. Si necesitas contestarle por el canal usa yujin.bridge.say con to:"'
|
|
2299
|
-
+ (fromPort || fromName) + '". NO reabras el canal salvo que aportes algo nuevo, para no entrar en loop.)';
|
|
2300
|
-
/* Fire-and-forget: the turn runs + streams to the panel via the normal SSE.
|
|
2301
|
-
* eco tier + origin tag + hop. */
|
|
2302
|
-
void fetch('http://127.0.0.1:' + ctx.port + '/api/chat', {
|
|
2303
|
-
method: 'POST',
|
|
2304
|
-
headers: { 'content-type': 'application/json' },
|
|
2305
|
-
body: JSON.stringify({
|
|
2306
|
-
messages: [{ role: 'user', content: tagged }],
|
|
2307
|
-
origin: 'bridge',
|
|
2308
|
-
brain_tier_override: 'eco',
|
|
2309
|
-
bridge_hop: hop,
|
|
2310
|
-
}),
|
|
2311
|
-
}).catch(() => undefined);
|
|
2312
|
-
sendJson(res, 200, { ok: true, accepted: true });
|
|
2542
|
+
catch { /* best-effort */ }
|
|
2543
|
+
void drainBridgeInbox(ctx);
|
|
2544
|
+
sendJson(res, 200, { ok: true });
|
|
2313
2545
|
return;
|
|
2314
2546
|
}
|
|
2315
2547
|
if (req.method === 'GET' && url.pathname === '/api/forge/wizard-state') {
|
|
@@ -6037,7 +6269,16 @@ async function handleChat(req, res, ctx) {
|
|
|
6037
6269
|
const _previos = sessionStore.messages();
|
|
6038
6270
|
const _cola = _previos[_previos.length - 1];
|
|
6039
6271
|
if (!_cola || _cola.role !== 'user' || _cola.content !== _nuevo.content) {
|
|
6040
|
-
|
|
6272
|
+
/* PND-224 -- la procedencia queda en el transcript como CAMPO. Hasta
|
|
6273
|
+
* hoy el unico rastro de que un turno venia de otra instancia era el
|
|
6274
|
+
* preambulo que el receptor le pegaba al prompt, y el recap de
|
|
6275
|
+
* arranque lo reconocia por esa cadena. Al dejar de editar el prompt,
|
|
6276
|
+
* la marca tiene que ser estructural o el recap empieza a contar las
|
|
6277
|
+
* coordinaciones como pedidos del dueno. No viaja al proveedor: cada
|
|
6278
|
+
* cliente mapea {role, content}. */
|
|
6279
|
+
const _origen = body.origin === 'bridge' || body.origin === 'mobile'
|
|
6280
|
+
? String(body.origin) : null;
|
|
6281
|
+
sessionStore.append(_origen ? { ..._nuevo, origin: _origen } : _nuevo);
|
|
6041
6282
|
void sessionStore.flush();
|
|
6042
6283
|
}
|
|
6043
6284
|
}
|
|
@@ -6054,9 +6295,16 @@ async function handleChat(req, res, ctx) {
|
|
|
6054
6295
|
* the extra fields and keep their current end-of-turn behavior. */
|
|
6055
6296
|
const _xTurnId = (body.origin === 'mobile' ? 'mb-' : body.origin === 'bridge' ? 'br-' : 'dt-') + Date.now().toString(36);
|
|
6056
6297
|
/* Queda registrado como ESTADO para que el latido de presencia lo
|
|
6057
|
-
* repita: ver el comentario largo en RelayPublisher.presence().
|
|
6058
|
-
|
|
6059
|
-
|
|
6298
|
+
* repita: ver el comentario largo en RelayPublisher.presence().
|
|
6299
|
+
*
|
|
6300
|
+
* PND-225 -- EL ORIGEN BRIDGE ENTRA ACA. Antes se excluia junto con
|
|
6301
|
+
* 'mobile', y las dos exclusiones NO tenian la misma razon: el telefono se
|
|
6302
|
+
* excluye porque YA muestra su propia burbuja (el turno nacio ahi), pero un
|
|
6303
|
+
* turno pedido por otra instancia no se mostraba en NINGUNA parte -- ni
|
|
6304
|
+
* como turno en vuelo en el panel, ni en el latido de presencia que lee el
|
|
6305
|
+
* telefono. Un turno al que no se le puede llegar es peor que un turno que
|
|
6306
|
+
* falla (es la misma leccion del mapa de turnos activos). */
|
|
6307
|
+
if (sessionId === DEFAULT_SESSION_ID && lastUserText && body.origin !== 'mobile') {
|
|
6060
6308
|
setInFlightTurn({ id: _xTurnId, user: lastUserText, started_at: new Date().toISOString() });
|
|
6061
6309
|
turnEcho = { id: _xTurnId, user: lastUserText };
|
|
6062
6310
|
}
|
|
@@ -6064,9 +6312,29 @@ async function handleChat(req, res, ctx) {
|
|
|
6064
6312
|
if (body.origin === 'mobile' || body.origin === 'bridge') {
|
|
6065
6313
|
try {
|
|
6066
6314
|
const { enqueuePanelDispatch } = await import('./panel_queue.js');
|
|
6067
|
-
enqueuePanelDispatch('system.remote-turn', {
|
|
6315
|
+
enqueuePanelDispatch('system.remote-turn', {
|
|
6316
|
+
source: String(body.origin), id: _xTurnId, user: lastUserText, reply: '',
|
|
6317
|
+
/* La procedencia como dato: el panel puede rotular la burbuja con
|
|
6318
|
+
* el emisor en vez de dejarla como un pedido anonimo. */
|
|
6319
|
+
...(body.origin === 'bridge' ? {
|
|
6320
|
+
bridge_from: body.bridge_from ? String(body.bridge_from) : undefined,
|
|
6321
|
+
bridge_from_port: body.bridge_from_port ? String(body.bridge_from_port) : undefined,
|
|
6322
|
+
} : {}),
|
|
6323
|
+
}, new Date().toISOString());
|
|
6068
6324
|
}
|
|
6069
6325
|
catch { /* best-effort */ }
|
|
6326
|
+
/* Y ADEMAS el latido, para el origen bridge: el panel recibe la
|
|
6327
|
+
* burbuja por la cola de arriba, el telefono necesita el estado. */
|
|
6328
|
+
if (body.origin === 'bridge') {
|
|
6329
|
+
try {
|
|
6330
|
+
const pub = getRelayPublisher();
|
|
6331
|
+
if (pub) {
|
|
6332
|
+
pub.status('working', { user: lastUserText, turn_id: _xTurnId });
|
|
6333
|
+
void pub.flush();
|
|
6334
|
+
}
|
|
6335
|
+
}
|
|
6336
|
+
catch { /* best-effort */ }
|
|
6337
|
+
}
|
|
6070
6338
|
}
|
|
6071
6339
|
else {
|
|
6072
6340
|
try {
|
|
@@ -6118,6 +6386,13 @@ async function handleChat(req, res, ctx) {
|
|
|
6118
6386
|
catch { /* missing yujin.forge.json -> phase_i, no-op */ }
|
|
6119
6387
|
const brainSelection = await selectBrainForTurn({
|
|
6120
6388
|
user_override: brainOverride,
|
|
6389
|
+
/* PND-223 -- quien pidio el override, para que el log no se lo atribuya
|
|
6390
|
+
* a un /eco que nadie tipeo. */
|
|
6391
|
+
override_source: brainOverride
|
|
6392
|
+
? (body.origin === 'bridge' ? 'bridge_preference'
|
|
6393
|
+
: body.origin === 'mobile' ? 'api_caller'
|
|
6394
|
+
: 'slash_command')
|
|
6395
|
+
: undefined,
|
|
6121
6396
|
caller_hint: 'handleChat',
|
|
6122
6397
|
user_text: lastUserText,
|
|
6123
6398
|
history: historyForGate,
|
|
@@ -6540,7 +6815,7 @@ async function handleChat(req, res, ctx) {
|
|
|
6540
6815
|
* the next turn. Reaches every brain via systemDynamic. */
|
|
6541
6816
|
const _autoApprove = !!(_limits.danger_zone && _limits.danger_zone.auto_approve_all === true);
|
|
6542
6817
|
const _cwdStrict = !!(_limits.shell && _limits.shell.cwd_boundary_strict === true);
|
|
6543
|
-
const approvalPostureSection = buildApprovalPostureSection(_autoApprove, _cwdStrict);
|
|
6818
|
+
const approvalPostureSection = buildApprovalPostureSection(_autoApprove, _cwdStrict, body.origin === 'bridge' ? 'bridge' : body.origin === 'mobile' ? 'mobile' : 'panel');
|
|
6544
6819
|
/* Auto-recall (Pablo 2026-06-21) -- inject a recap of the recent WORK
|
|
6545
6820
|
* SESSIONS for this project so the model boots knowing what we did the
|
|
6546
6821
|
* last few days, not only general project knowledge. No LLM call; built
|
|
@@ -6709,6 +6984,11 @@ async function handleChat(req, res, ctx) {
|
|
|
6709
6984
|
projectSlug: ctx.projectSlug,
|
|
6710
6985
|
bridge: ctx.bridge,
|
|
6711
6986
|
bridgeHop,
|
|
6987
|
+
/* PND-221 -- el origen del turno viaja hasta el dispatcher porque la
|
|
6988
|
+
* zona de peligro NO se aplica a lo que pidio otro proceso. Ver
|
|
6989
|
+
* resolveDangerZone en src/nac3/danger_zone.ts. */
|
|
6990
|
+
turnOrigin: (body.origin === 'bridge' || body.origin === 'mobile'
|
|
6991
|
+
? String(body.origin) : 'panel'),
|
|
6712
6992
|
...(sseEmit ? { streamEmit: sseEmit } : {}),
|
|
6713
6993
|
/* per-session reanchor for non-default sessions; global for notebook */
|
|
6714
6994
|
...(effReanchor ? { reanchor: effReanchor } : {}),
|
|
@@ -7964,21 +8244,34 @@ multiagentEnabled = false) {
|
|
|
7964
8244
|
* this reflects the ACTUAL danger_zone.auto_approve_all + shell.cwd_boundary_
|
|
7965
8245
|
* strict config so the model only mentions approval when a modal will really
|
|
7966
8246
|
* appear. Read per-turn -> toggling the switch takes effect next turn. */
|
|
7967
|
-
export function buildApprovalPostureSection(autoApprove, cwdStrict) {
|
|
8247
|
+
export function buildApprovalPostureSection(autoApprove, cwdStrict, turnOrigin) {
|
|
7968
8248
|
/* Prompt audit E (2026-09-09): only inject a posture block when the LIVE config
|
|
7969
8249
|
* DEVIATES from the static R8 default (destructive verbs open a modal; shell
|
|
7970
8250
|
* free). When it matches R8 -- the common case -- say nothing: repeating "a
|
|
7971
8251
|
* modal opens" and "this OVERRIDES R8" every turn just diluted R8 and burned
|
|
7972
8252
|
* ~120 tok/turn. The block appears only for the real overrides. */
|
|
7973
|
-
|
|
8253
|
+
const fromBridge = turnOrigin === 'bridge';
|
|
8254
|
+
if (!autoApprove && !cwdStrict && !fromBridge)
|
|
7974
8255
|
return '';
|
|
7975
8256
|
const lines = ['\n\nRUNTIME APPROVAL POSTURE (live config -- deviates from the R8 default):'];
|
|
7976
|
-
if (autoApprove) {
|
|
8257
|
+
if (autoApprove && !fromBridge) {
|
|
7977
8258
|
lines.push('- danger_zone.auto_approve_all = TRUE (autonomous). Destructive verbs '
|
|
7978
8259
|
+ '(git push/commit, rm, fs.write-project-file, shell.exec, ...) EXECUTE '
|
|
7979
8260
|
+ 'IMMEDIATELY with NO approval modal -- this SUSPENDS R8. Do NOT tell the user '
|
|
7980
8261
|
+ 'to approve or say "a modal appears"; just invoke and report.');
|
|
7981
8262
|
}
|
|
8263
|
+
if (fromBridge) {
|
|
8264
|
+
/* PND-221 -- decirselo al cerebro en el mismo turno, porque si no narra
|
|
8265
|
+
* "ya lo ejecute" sobre algo que quedo esperando un modal. */
|
|
8266
|
+
lines.push('- THIS TURN CAME FROM ANOTHER FORGE INSTANCE (bridge). R8 is IN FORCE '
|
|
8267
|
+
+ 'for it'
|
|
8268
|
+
+ (autoApprove
|
|
8269
|
+
? ' even though danger_zone.auto_approve_all = TRUE: that switch is the owner\'s '
|
|
8270
|
+
+ 'decision ABOUT HIMSELF and does not extend to what another process asks. '
|
|
8271
|
+
: '. ')
|
|
8272
|
+
+ 'Every destructive / approval-required verb opens the modal and waits. Do NOT '
|
|
8273
|
+
+ 'report such an action as done until the modal comes back APPROVED.');
|
|
8274
|
+
}
|
|
7982
8275
|
if (cwdStrict) {
|
|
7983
8276
|
lines.push('- shell.cwd_boundary_strict = TRUE. Shell is confined to the project '
|
|
7984
8277
|
+ 'root; commands targeting paths outside it are blocked.');
|