@nac3/forge-cli 1.16.0 → 1.17.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/chat/panel.d.ts.map +1 -1
- package/dist/chat/panel.js +67 -0
- package/dist/chat/panel.js.map +1 -1
- package/dist/chat/persistence.d.ts +3 -0
- package/dist/chat/persistence.d.ts.map +1 -1
- package/dist/chat/persistence.js +5 -0
- package/dist/chat/persistence.js.map +1 -1
- package/dist/chat/server.d.ts.map +1 -1
- package/dist/chat/server.js +41 -2
- package/dist/chat/server.js.map +1 -1
- package/dist/chat/session_restore.d.ts +90 -0
- package/dist/chat/session_restore.d.ts.map +1 -0
- package/dist/chat/session_restore.js +71 -0
- package/dist/chat/session_restore.js.map +1 -0
- package/dist/relay/in_flight_turn.d.ts +42 -9
- package/dist/relay/in_flight_turn.d.ts.map +1 -1
- package/dist/relay/in_flight_turn.js +57 -13
- package/dist/relay/in_flight_turn.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reatar el panel a la sesion viva.
|
|
3
|
+
*
|
|
4
|
+
* POR QUE EXISTE (Pablo, 13-oct-2026): se le cerro por error la ventana de
|
|
5
|
+
* Chrome del panel con un turno de E7.5 corriendo. El turno NO se perdio -- el
|
|
6
|
+
* servidor es un proceso aparte y siguio hasta el final -- pero al reabrir el
|
|
7
|
+
* panel estaba VACIO, y la pestana Coding tampoco volvio.
|
|
8
|
+
*
|
|
9
|
+
* Las dos perdidas tienen la misma causa: el panel guardaba en el navegador
|
|
10
|
+
* solo `yf-lang` y `yf_coding_mode`, el transcript renderizado era estado en
|
|
11
|
+
* memoria de ESA ventana, y no habia ningun endpoint que lo reconstruyera
|
|
12
|
+
* (medido: ninguno de los 25 que el panel consume sirve historial). Y el
|
|
13
|
+
* lienzo Coding se abre cuando LLEGAN eventos de actividad, asi que una
|
|
14
|
+
* ventana nueva, que no esta enganchada al stream SSE del turno en vuelo, no
|
|
15
|
+
* tiene de donde enterarse de que hay algo corriendo.
|
|
16
|
+
*
|
|
17
|
+
* O sea que no era "el historial no persiste": era "la ventana nueva no se
|
|
18
|
+
* reata a la sesion". Un solo endpoint arregla las dos cosas, porque las dos
|
|
19
|
+
* necesitan el mismo dato: que dijo la conversacion, y si hay un turno en
|
|
20
|
+
* vuelo ahora.
|
|
21
|
+
*
|
|
22
|
+
* Lo que este modulo NO hace: inventar durabilidad. El transcript ya se
|
|
23
|
+
* persiste en `.yujin-forge/cache/chat/` desde la primera version (SPEC 5) y
|
|
24
|
+
* el turno en vuelo ya es un estado consultable (`in_flight_turn.ts`). Lo que
|
|
25
|
+
* faltaba era servirlos.
|
|
26
|
+
*
|
|
27
|
+
* ASCII-only.
|
|
28
|
+
*/
|
|
29
|
+
import type { ChatMessage } from './claude.js';
|
|
30
|
+
import type { InFlightTurn } from '../relay/in_flight_turn.js';
|
|
31
|
+
/**
|
|
32
|
+
* Cuantos mensajes se devuelven, desde el final.
|
|
33
|
+
*
|
|
34
|
+
* Por que un tope y no la sesion entera: una sesion de un dia entero son
|
|
35
|
+
* megabytes, y el panel ya recorta lo que PINTA. Devolver todo haria del
|
|
36
|
+
* arranque del panel la peticion mas cara del producto para mostrar lo mismo.
|
|
37
|
+
* 60 es el mismo numero que usa el espejo del telefono, por consistencia.
|
|
38
|
+
*/
|
|
39
|
+
export declare const TOPE_MENSAJES = 60;
|
|
40
|
+
/**
|
|
41
|
+
* Tope de bytes del payload. El tope de mensajes no alcanza: un solo turno con
|
|
42
|
+
* un volcado de consola pegado adentro puede ser de megabytes (hoy mismo hubo
|
|
43
|
+
* uno de 11.870 caracteres). Se recorta desde el final -- lo mas nuevo es lo
|
|
44
|
+
* que el usuario quiere ver.
|
|
45
|
+
*/
|
|
46
|
+
export declare const TOPE_BYTES: number;
|
|
47
|
+
export interface MensajeRestaurado {
|
|
48
|
+
role: 'user' | 'assistant';
|
|
49
|
+
content: string;
|
|
50
|
+
}
|
|
51
|
+
export interface TurnoEnVuelo {
|
|
52
|
+
id: string;
|
|
53
|
+
user: string;
|
|
54
|
+
started_at: string;
|
|
55
|
+
/** Segundos que lleva corriendo, para que el panel pueda decir "3h en curso"
|
|
56
|
+
* sin tener que calcularlo contra un reloj que puede no coincidir. */
|
|
57
|
+
elapsed_s: number;
|
|
58
|
+
}
|
|
59
|
+
export interface PayloadRestore {
|
|
60
|
+
ok: true;
|
|
61
|
+
/** Los mensajes, del mas viejo al mas nuevo, ya recortados. */
|
|
62
|
+
messages: MensajeRestaurado[];
|
|
63
|
+
/** True cuando se dejaron mensajes afuera por alguno de los dos topes. */
|
|
64
|
+
truncated: boolean;
|
|
65
|
+
/** Cuantos mensajes tiene la sesion en total. */
|
|
66
|
+
total: number;
|
|
67
|
+
/** El turno en vuelo, o null. Es lo que hace que el panel reabierto muestre
|
|
68
|
+
* la burbuja pendiente Y vuelva a abrir el lienzo Coding. */
|
|
69
|
+
in_flight: TurnoEnVuelo | null;
|
|
70
|
+
project_root: string;
|
|
71
|
+
session_started_at: string | null;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Arma el payload. Pura: toda la entrada es argumento, y por eso se puede
|
|
75
|
+
* probar el recorte sin levantar un servidor.
|
|
76
|
+
*
|
|
77
|
+
* `redactar` entra como funcion y no se importa adentro para que el test pueda
|
|
78
|
+
* afirmar QUE SE LLAMO. El transcript en disco ya se escribe redactado, pero
|
|
79
|
+
* los mensajes en MEMORIA no pasaron por ahi todavia, y este endpoint los
|
|
80
|
+
* sirve: redactar aca no es ceremonia, es el unico lugar donde pasa.
|
|
81
|
+
*/
|
|
82
|
+
export declare function construirRestore(args: {
|
|
83
|
+
messages: readonly ChatMessage[];
|
|
84
|
+
inFlight: InFlightTurn | null;
|
|
85
|
+
projectRoot: string;
|
|
86
|
+
sessionStartedAt?: string | null;
|
|
87
|
+
now?: number;
|
|
88
|
+
redactar?: (s: string) => string;
|
|
89
|
+
}): PayloadRestore;
|
|
90
|
+
//# sourceMappingURL=session_restore.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session_restore.d.ts","sourceRoot":"","sources":["../../src/chat/session_restore.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC/C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAE/D;;;;;;;GAOG;AACH,eAAO,MAAM,aAAa,KAAK,CAAC;AAEhC;;;;;GAKG;AACH,eAAO,MAAM,UAAU,QAAa,CAAC;AAErC,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,MAAM,GAAG,WAAW,CAAC;IAC3B,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB;2EACuE;IACvE,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,IAAI,CAAC;IACT,+DAA+D;IAC/D,QAAQ,EAAE,iBAAiB,EAAE,CAAC;IAC9B,0EAA0E;IAC1E,SAAS,EAAE,OAAO,CAAC;IACnB,iDAAiD;IACjD,KAAK,EAAE,MAAM,CAAC;IACd;kEAC8D;IAC9D,SAAS,EAAE,YAAY,GAAG,IAAI,CAAC;IAC/B,YAAY,EAAE,MAAM,CAAC;IACrB,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAAC;CACnC;AAOD;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE;IACrC,QAAQ,EAAE,SAAS,WAAW,EAAE,CAAC;IACjC,QAAQ,EAAE,YAAY,GAAG,IAAI,CAAC;IAC9B,WAAW,EAAE,MAAM,CAAC;IACpB,gBAAgB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,MAAM,CAAC;CAClC,GAAG,cAAc,CA8CjB"}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cuantos mensajes se devuelven, desde el final.
|
|
3
|
+
*
|
|
4
|
+
* Por que un tope y no la sesion entera: una sesion de un dia entero son
|
|
5
|
+
* megabytes, y el panel ya recorta lo que PINTA. Devolver todo haria del
|
|
6
|
+
* arranque del panel la peticion mas cara del producto para mostrar lo mismo.
|
|
7
|
+
* 60 es el mismo numero que usa el espejo del telefono, por consistencia.
|
|
8
|
+
*/
|
|
9
|
+
export const TOPE_MENSAJES = 60;
|
|
10
|
+
/**
|
|
11
|
+
* Tope de bytes del payload. El tope de mensajes no alcanza: un solo turno con
|
|
12
|
+
* un volcado de consola pegado adentro puede ser de megabytes (hoy mismo hubo
|
|
13
|
+
* uno de 11.870 caracteres). Se recorta desde el final -- lo mas nuevo es lo
|
|
14
|
+
* que el usuario quiere ver.
|
|
15
|
+
*/
|
|
16
|
+
export const TOPE_BYTES = 512 * 1024;
|
|
17
|
+
/** Cuantos bytes ocupa en el payload (UTF-8, mas el envoltorio JSON). */
|
|
18
|
+
function pesa(m) {
|
|
19
|
+
return Buffer.byteLength(m.content, 'utf8') + Buffer.byteLength(m.role, 'utf8') + 24;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Arma el payload. Pura: toda la entrada es argumento, y por eso se puede
|
|
23
|
+
* probar el recorte sin levantar un servidor.
|
|
24
|
+
*
|
|
25
|
+
* `redactar` entra como funcion y no se importa adentro para que el test pueda
|
|
26
|
+
* afirmar QUE SE LLAMO. El transcript en disco ya se escribe redactado, pero
|
|
27
|
+
* los mensajes en MEMORIA no pasaron por ahi todavia, y este endpoint los
|
|
28
|
+
* sirve: redactar aca no es ceremonia, es el unico lugar donde pasa.
|
|
29
|
+
*/
|
|
30
|
+
export function construirRestore(args) {
|
|
31
|
+
const { messages, inFlight, projectRoot } = args;
|
|
32
|
+
const now = args.now ?? Date.now();
|
|
33
|
+
const redactar = args.redactar ?? ((s) => s);
|
|
34
|
+
/* Solo lo que el panel pinta como conversacion. Un mensaje sin contenido no
|
|
35
|
+
* aporta una burbuja: aporta una vacia. */
|
|
36
|
+
const utiles = messages.filter((m) => (m.role === 'user' || m.role === 'assistant') && typeof m.content === 'string' && m.content !== '');
|
|
37
|
+
const porTope = utiles.slice(-TOPE_MENSAJES);
|
|
38
|
+
/* El recorte por bytes va desde el final hacia atras. */
|
|
39
|
+
const elegidos = [];
|
|
40
|
+
let bytes = 0;
|
|
41
|
+
for (let i = porTope.length - 1; i >= 0; i--) {
|
|
42
|
+
const m = porTope[i];
|
|
43
|
+
const item = { role: m.role, content: redactar(m.content) };
|
|
44
|
+
const b = pesa(item);
|
|
45
|
+
if (bytes + b > TOPE_BYTES && elegidos.length > 0)
|
|
46
|
+
break;
|
|
47
|
+
elegidos.unshift(item);
|
|
48
|
+
bytes += b;
|
|
49
|
+
}
|
|
50
|
+
let enVuelo = null;
|
|
51
|
+
if (inFlight && inFlight.id) {
|
|
52
|
+
const desde = Date.parse(inFlight.started_at);
|
|
53
|
+
const elapsed = Number.isFinite(desde) ? Math.max(0, Math.round((now - desde) / 1000)) : 0;
|
|
54
|
+
enVuelo = {
|
|
55
|
+
id: inFlight.id,
|
|
56
|
+
user: redactar(inFlight.user),
|
|
57
|
+
started_at: inFlight.started_at,
|
|
58
|
+
elapsed_s: elapsed,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
ok: true,
|
|
63
|
+
messages: elegidos,
|
|
64
|
+
truncated: elegidos.length < utiles.length,
|
|
65
|
+
total: utiles.length,
|
|
66
|
+
in_flight: enVuelo,
|
|
67
|
+
project_root: projectRoot,
|
|
68
|
+
session_started_at: args.sessionStartedAt ?? null,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
//# sourceMappingURL=session_restore.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session_restore.js","sourceRoot":"","sources":["../../src/chat/session_restore.ts"],"names":[],"mappings":"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"}
|
|
@@ -33,23 +33,56 @@ export interface InFlightTurn {
|
|
|
33
33
|
started_at: string;
|
|
34
34
|
}
|
|
35
35
|
/**
|
|
36
|
-
*
|
|
37
|
-
*
|
|
36
|
+
* Cada cuanto el turno da senal de vida. Es el periodo del latido de actividad
|
|
37
|
+
* del servidor (`startRelayHeartbeat`), que corre SOLO entre el arranque y el
|
|
38
|
+
* cierre del turno.
|
|
38
39
|
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
* Mentir sobre lo que el sistema esta haciendo es peor que no decir nada.
|
|
40
|
+
* Vive aca y no en server.ts a proposito: es la mitad de un par acoplado, y la
|
|
41
|
+
* otra mitad es `SILENCIO_MAX_MS`. El bug anterior nacio de tener dos numeros
|
|
42
|
+
* acoplados en dos archivos distintos sin nada que los ligara.
|
|
43
43
|
*/
|
|
44
|
-
export declare const
|
|
45
|
-
|
|
44
|
+
export declare const PULSO_DE_PROGRESO_MS: number;
|
|
45
|
+
/**
|
|
46
|
+
* Tope de SILENCIO. Un turno que no da senal de vida por mas que esto se
|
|
47
|
+
* considera muerto y deja de anunciarse, aunque nadie lo haya limpiado.
|
|
48
|
+
*
|
|
49
|
+
* ANTES ERA UN TOPE DE EDAD, y eso estaba mal (Pablo, 13-oct-2026: "hay turnos
|
|
50
|
+
* de mas de 3 horas en un desarrollo"). La proteccion que se queria es contra
|
|
51
|
+
* un turno COLGADO -- un proceso que muere a mitad de camino, o una excepcion
|
|
52
|
+
* por un camino que no pasa por `clearInFlightTurn` -- porque mentir sobre lo
|
|
53
|
+
* que el sistema esta haciendo es peor que no decir nada. Pero "colgado" no es
|
|
54
|
+
* "viejo": es "sin progreso". Un tope de edad confunde las dos cosas y castiga
|
|
55
|
+
* justo al turno largo, que es el que mas necesita anunciarse.
|
|
56
|
+
*
|
|
57
|
+
* Medido: un turno de 29 minutos en ADIP-Kikan dejaba al telefono diciendo que
|
|
58
|
+
* no habia nada corriendo durante sus ultimos 19 minutos. Con turnos de 3 horas
|
|
59
|
+
* el anuncio serviria para el 5% del turno.
|
|
60
|
+
*
|
|
61
|
+
* Diez pulsos de margen: tolera que se pierdan nueve latidos seguidos antes de
|
|
62
|
+
* dar por muerto un turno que esta vivo.
|
|
63
|
+
*/
|
|
64
|
+
export declare const SILENCIO_MAX_MS: number;
|
|
65
|
+
export declare function setInFlightTurn(t: InFlightTurn, now?: number): void;
|
|
66
|
+
/**
|
|
67
|
+
* Senal de vida del turno en curso. La llama el latido de actividad del
|
|
68
|
+
* servidor, que corre solo mientras hay turno.
|
|
69
|
+
*
|
|
70
|
+
* No crea nada: si no hay turno registrado, no hace nada. Asi un latido
|
|
71
|
+
* rezagado no puede resucitar un turno ya cerrado.
|
|
72
|
+
*/
|
|
73
|
+
export declare function touchInFlightTurn(now?: number): void;
|
|
46
74
|
/**
|
|
47
75
|
* Limpia el turno en vuelo. Con `id`, SOLO si coincide: dos turnos encadenados
|
|
48
76
|
* pueden cerrar fuera de orden, y un cierre tardio del viejo no tiene que
|
|
49
77
|
* borrar el anuncio del que recien arranco.
|
|
50
78
|
*/
|
|
51
79
|
export declare function clearInFlightTurn(id?: string): void;
|
|
52
|
-
/**
|
|
80
|
+
/**
|
|
81
|
+
* El turno en vuelo, o null si no hay o si se quedo CALLADO.
|
|
82
|
+
*
|
|
83
|
+
* La edad del turno no entra en la decision: un turno de tres horas que sigue
|
|
84
|
+
* latiendo es un turno vivo y se anuncia. Lo que lo saca es el silencio.
|
|
85
|
+
*/
|
|
53
86
|
export declare function inFlightTurn(now?: number): InFlightTurn | null;
|
|
54
87
|
/** Solo para tests. */
|
|
55
88
|
export declare function _resetInFlightTurn(): void;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"in_flight_turn.d.ts","sourceRoot":"","sources":["../../src/relay/in_flight_turn.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,wBAAwB;IACxB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,oBAAoB,
|
|
1
|
+
{"version":3,"file":"in_flight_turn.d.ts","sourceRoot":"","sources":["../../src/relay/in_flight_turn.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,wBAAwB;IACxB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,oBAAoB,QAAY,CAAC;AAE9C;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,eAAe,QAA4B,CAAC;AASzD,wBAAgB,eAAe,CAAC,CAAC,EAAE,YAAY,EAAE,GAAG,GAAE,MAAmB,GAAG,IAAI,CAQ/E;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,GAAE,MAAmB,GAAG,IAAI,CAGhE;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,EAAE,CAAC,EAAE,MAAM,GAAG,IAAI,CAInD;AAED;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,GAAG,GAAE,MAAmB,GAAG,YAAY,GAAG,IAAI,CAU1E;AAED,uBAAuB;AACvB,wBAAgB,kBAAkB,IAAI,IAAI,CAEzC"}
|
|
@@ -27,20 +27,57 @@
|
|
|
27
27
|
* ASCII puro.
|
|
28
28
|
*/
|
|
29
29
|
/**
|
|
30
|
-
*
|
|
31
|
-
*
|
|
30
|
+
* Cada cuanto el turno da senal de vida. Es el periodo del latido de actividad
|
|
31
|
+
* del servidor (`startRelayHeartbeat`), que corre SOLO entre el arranque y el
|
|
32
|
+
* cierre del turno.
|
|
32
33
|
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
* Mentir sobre lo que el sistema esta haciendo es peor que no decir nada.
|
|
34
|
+
* Vive aca y no en server.ts a proposito: es la mitad de un par acoplado, y la
|
|
35
|
+
* otra mitad es `SILENCIO_MAX_MS`. El bug anterior nacio de tener dos numeros
|
|
36
|
+
* acoplados en dos archivos distintos sin nada que los ligara.
|
|
37
37
|
*/
|
|
38
|
-
export const
|
|
38
|
+
export const PULSO_DE_PROGRESO_MS = 60 * 1000;
|
|
39
|
+
/**
|
|
40
|
+
* Tope de SILENCIO. Un turno que no da senal de vida por mas que esto se
|
|
41
|
+
* considera muerto y deja de anunciarse, aunque nadie lo haya limpiado.
|
|
42
|
+
*
|
|
43
|
+
* ANTES ERA UN TOPE DE EDAD, y eso estaba mal (Pablo, 13-oct-2026: "hay turnos
|
|
44
|
+
* de mas de 3 horas en un desarrollo"). La proteccion que se queria es contra
|
|
45
|
+
* un turno COLGADO -- un proceso que muere a mitad de camino, o una excepcion
|
|
46
|
+
* por un camino que no pasa por `clearInFlightTurn` -- porque mentir sobre lo
|
|
47
|
+
* que el sistema esta haciendo es peor que no decir nada. Pero "colgado" no es
|
|
48
|
+
* "viejo": es "sin progreso". Un tope de edad confunde las dos cosas y castiga
|
|
49
|
+
* justo al turno largo, que es el que mas necesita anunciarse.
|
|
50
|
+
*
|
|
51
|
+
* Medido: un turno de 29 minutos en ADIP-Kikan dejaba al telefono diciendo que
|
|
52
|
+
* no habia nada corriendo durante sus ultimos 19 minutos. Con turnos de 3 horas
|
|
53
|
+
* el anuncio serviria para el 5% del turno.
|
|
54
|
+
*
|
|
55
|
+
* Diez pulsos de margen: tolera que se pierdan nueve latidos seguidos antes de
|
|
56
|
+
* dar por muerto un turno que esta vivo.
|
|
57
|
+
*/
|
|
58
|
+
export const SILENCIO_MAX_MS = PULSO_DE_PROGRESO_MS * 10;
|
|
39
59
|
let current = null;
|
|
40
|
-
export function setInFlightTurn(t) {
|
|
60
|
+
export function setInFlightTurn(t, now = Date.now()) {
|
|
41
61
|
if (!t || !t.id)
|
|
42
62
|
return;
|
|
43
|
-
current = {
|
|
63
|
+
current = {
|
|
64
|
+
id: String(t.id),
|
|
65
|
+
user: String(t.user || ''),
|
|
66
|
+
started_at: String(t.started_at || new Date(now).toISOString()),
|
|
67
|
+
visto: now,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Senal de vida del turno en curso. La llama el latido de actividad del
|
|
72
|
+
* servidor, que corre solo mientras hay turno.
|
|
73
|
+
*
|
|
74
|
+
* No crea nada: si no hay turno registrado, no hace nada. Asi un latido
|
|
75
|
+
* rezagado no puede resucitar un turno ya cerrado.
|
|
76
|
+
*/
|
|
77
|
+
export function touchInFlightTurn(now = Date.now()) {
|
|
78
|
+
if (!current)
|
|
79
|
+
return;
|
|
80
|
+
current.visto = now;
|
|
44
81
|
}
|
|
45
82
|
/**
|
|
46
83
|
* Limpia el turno en vuelo. Con `id`, SOLO si coincide: dos turnos encadenados
|
|
@@ -54,16 +91,23 @@ export function clearInFlightTurn(id) {
|
|
|
54
91
|
return;
|
|
55
92
|
current = null;
|
|
56
93
|
}
|
|
57
|
-
/**
|
|
94
|
+
/**
|
|
95
|
+
* El turno en vuelo, o null si no hay o si se quedo CALLADO.
|
|
96
|
+
*
|
|
97
|
+
* La edad del turno no entra en la decision: un turno de tres horas que sigue
|
|
98
|
+
* latiendo es un turno vivo y se anuncia. Lo que lo saca es el silencio.
|
|
99
|
+
*/
|
|
58
100
|
export function inFlightTurn(now = Date.now()) {
|
|
59
101
|
if (!current)
|
|
60
102
|
return null;
|
|
61
|
-
|
|
62
|
-
|
|
103
|
+
/* Un reloj que salta hacia atras da silencio negativo: no vence. Preferible
|
|
104
|
+
* anunciar un turno de mas que descartar uno vivo por un salto de NTP. */
|
|
105
|
+
if (now - current.visto > SILENCIO_MAX_MS) {
|
|
63
106
|
current = null;
|
|
64
107
|
return null;
|
|
65
108
|
}
|
|
66
|
-
|
|
109
|
+
const { id, user, started_at } = current;
|
|
110
|
+
return { id, user, started_at };
|
|
67
111
|
}
|
|
68
112
|
/** Solo para tests. */
|
|
69
113
|
export function _resetInFlightTurn() {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"in_flight_turn.js","sourceRoot":"","sources":["../../src/relay/in_flight_turn.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AASH;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAG,EAAE,GAAG,
|
|
1
|
+
{"version":3,"file":"in_flight_turn.js","sourceRoot":"","sources":["../../src/relay/in_flight_turn.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AASH;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAG,EAAE,GAAG,IAAI,CAAC;AAE9C;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG,oBAAoB,GAAG,EAAE,CAAC;AAOzD,IAAI,OAAO,GAAoB,IAAI,CAAC;AAEpC,MAAM,UAAU,eAAe,CAAC,CAAe,EAAE,MAAc,IAAI,CAAC,GAAG,EAAE;IACvE,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE;QAAE,OAAO;IACxB,OAAO,GAAG;QACR,EAAE,EAAE,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC;QAChB,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC,IAAI,IAAI,EAAE,CAAC;QAC1B,UAAU,EAAE,MAAM,CAAC,CAAC,CAAC,UAAU,IAAI,IAAI,IAAI,CAAC,GAAG,CAAC,CAAC,WAAW,EAAE,CAAC;QAC/D,KAAK,EAAE,GAAG;KACX,CAAC;AACJ,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,iBAAiB,CAAC,MAAc,IAAI,CAAC,GAAG,EAAE;IACxD,IAAI,CAAC,OAAO;QAAE,OAAO;IACrB,OAAO,CAAC,KAAK,GAAG,GAAG,CAAC;AACtB,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,iBAAiB,CAAC,EAAW;IAC3C,IAAI,CAAC,OAAO;QAAE,OAAO;IACrB,IAAI,EAAE,IAAI,OAAO,CAAC,EAAE,KAAK,EAAE;QAAE,OAAO;IACpC,OAAO,GAAG,IAAI,CAAC;AACjB,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,YAAY,CAAC,MAAc,IAAI,CAAC,GAAG,EAAE;IACnD,IAAI,CAAC,OAAO;QAAE,OAAO,IAAI,CAAC;IAC1B;8EAC0E;IAC1E,IAAI,GAAG,GAAG,OAAO,CAAC,KAAK,GAAG,eAAe,EAAE,CAAC;QAC1C,OAAO,GAAG,IAAI,CAAC;QACf,OAAO,IAAI,CAAC;IACd,CAAC;IACD,MAAM,EAAE,EAAE,EAAE,IAAI,EAAE,UAAU,EAAE,GAAG,OAAO,CAAC;IACzC,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,UAAU,EAAE,CAAC;AAClC,CAAC;AAED,uBAAuB;AACvB,MAAM,UAAU,kBAAkB;IAChC,OAAO,GAAG,IAAI,CAAC;AACjB,CAAC"}
|
package/dist/version.d.ts
CHANGED
|
@@ -2,5 +2,5 @@
|
|
|
2
2
|
* Yujin Forge CLI version. Kept in sync with package.json by the npm
|
|
3
3
|
* "version" lifecycle (scripts/sync_version.mjs) so the two never drift.
|
|
4
4
|
*/
|
|
5
|
-
export declare const VERSION = "1.
|
|
5
|
+
export declare const VERSION = "1.17.0";
|
|
6
6
|
//# sourceMappingURL=version.d.ts.map
|
package/dist/version.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nac3/forge-cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.17.0",
|
|
4
4
|
"description": "Yujin Forge -- voice-first NAC-3 React development framework. CLI + chat panel + spec ingest + 10-format document reader + voice loop.",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE",
|
|
6
6
|
"author": "Pablo Kuschnirof <pablo@rpaforce.com>",
|