chocolatito-code 1.6.11 → 1.6.13

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.
@@ -0,0 +1,41 @@
1
+ /**
2
+ * LA CAJA NEGRA DE LOS CORTES
3
+ *
4
+ * DE DONDE SALE
5
+ *
6
+ * Cinco respuestas seguidas cortadas a mitad de frase, y de todas ellas no
7
+ * quedo NADA con lo que trabajar: el aviso salia por pantalla, el usuario
8
+ * seguia tecleando y el rastro se perdia con el scroll. Al dia siguiente, la
9
+ * unica pregunta que importaba -"corta siempre por el mismo sitio?"- no se
10
+ * podia contestar.
11
+ *
12
+ * Aqui se apunta lo justo para poder contestarla la proxima vez, que es un
13
+ * renglon por corte:
14
+ *
15
+ * 2026-09-12T14:02:11Z modelo=deepseek-chat ms=1840 trozos=37
16
+ * texto=214 razonamiento=0 aviso=motor
17
+ *
18
+ * Con eso se distingue lo que hoy no se distingue:
19
+ *
20
+ * - Siempre a los mismos milisegundos -> algo lo esta cortando por tiempo.
21
+ * - Siempre en los mismos bytes -> un limite de tamano por el camino.
22
+ * - Numeros dispares -> el motor de arriba, que va a rachas.
23
+ * - `aviso=motor` -> lo dijo el proxy: el corte viene del motor.
24
+ * - `aviso=mudo` -> nadie dijo nada: se murio el proxy por el camino.
25
+ *
26
+ * NO se guarda ni una palabra de la conversacion. Solo cuentas: tamanos,
27
+ * tiempos y el nombre del motor. Un diagnostico no vale una fuga.
28
+ */
29
+ export interface Corte {
30
+ modelo: string;
31
+ ms: number;
32
+ trozos: number;
33
+ bytesTexto: number;
34
+ bytesRazonamiento: number;
35
+ /** "motor" si el proxy lo anuncio; "mudo" si el stream murio sin decir nada. */
36
+ aviso: "motor" | "mudo";
37
+ }
38
+ export declare function rutaDelRegistro(home?: string): string;
39
+ export declare function apuntarCorte(corte: Corte, home?: string): string | null;
40
+ /** Lo apuntado, de lo mas nuevo a lo mas viejo. Para /cortes. */
41
+ export declare function leerCortes(limite?: number, home?: string): string[];
@@ -0,0 +1,52 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ export function rutaDelRegistro(home = os.homedir()) {
5
+ return path.join(home, ".chocolatito", "cortes.log");
6
+ }
7
+ /** Cuantos cortes se guardan. Mas atras que esto ya no dice nada nuevo. */
8
+ const MAXIMO_DE_LINEAS = 200;
9
+ export function apuntarCorte(corte, home) {
10
+ const ruta = rutaDelRegistro(home);
11
+ const linea = [
12
+ new Date().toISOString(),
13
+ `modelo=${corte.modelo}`,
14
+ `ms=${corte.ms}`,
15
+ `trozos=${corte.trozos}`,
16
+ `texto=${corte.bytesTexto}`,
17
+ `razonamiento=${corte.bytesRazonamiento}`,
18
+ `aviso=${corte.aviso}`,
19
+ ].join(" ") + "\n";
20
+ try {
21
+ fs.mkdirSync(path.dirname(ruta), { recursive: true });
22
+ let previo = "";
23
+ try {
24
+ previo = fs.readFileSync(ruta, "utf-8");
25
+ }
26
+ catch { }
27
+ // Se recorta al vuelo: un registro que crece sin fin acaba siendo otro
28
+ // problema, y aqui lo viejo no aporta.
29
+ const lineas = (previo + linea).split("\n").filter((l) => l.trim().length > 0);
30
+ const recortado = lineas.slice(-MAXIMO_DE_LINEAS).join("\n") + "\n";
31
+ fs.writeFileSync(ruta, recortado, "utf-8");
32
+ return ruta;
33
+ }
34
+ catch {
35
+ // Si no se puede escribir, no pasa nada: esto es un extra, no el aviso.
36
+ return null;
37
+ }
38
+ }
39
+ /** Lo apuntado, de lo mas nuevo a lo mas viejo. Para /cortes. */
40
+ export function leerCortes(limite = 20, home) {
41
+ try {
42
+ return fs
43
+ .readFileSync(rutaDelRegistro(home), "utf-8")
44
+ .split("\n")
45
+ .filter((l) => l.trim().length > 0)
46
+ .slice(-limite)
47
+ .reverse();
48
+ }
49
+ catch {
50
+ return [];
51
+ }
52
+ }
@@ -56,3 +56,27 @@ export declare function liberarHuecos(): void;
56
56
  * que hacer nada: es lo unico que se arregla solo con esperar.
57
57
  */
58
58
  export declare function mensajeDeEspera(segundos: number): string;
59
+ /**
60
+ * Espera a que se libere el hueco, en vez de devolverle la pared al usuario.
61
+ *
62
+ * DE DONDE SALE
63
+ *
64
+ * La misma sesion, un paso mas alla. Ya no se gastaba una peticion por intento
65
+ * -eso lo arreglo lo de arriba-, pero el usuario seguia atrapado:
66
+ *
67
+ * > sigue
68
+ * ⏸ Faltan 144s.
69
+ * > sigoe
70
+ * ⏸ Faltan 133s.
71
+ *
72
+ * Tecleando contra una pared que el programa sabia medir. Si sabemos que faltan
73
+ * 133 segundos, esperarlos es trabajo del programa, no del que paga. Se avisa,
74
+ * se cuenta hacia atras y se sigue solo; con Esc se deja.
75
+ *
76
+ * Vuelve `true` si ya hay hueco y `false` si el usuario corto.
77
+ */
78
+ export declare function esperarAlHueco(opciones: {
79
+ signal?: AbortSignal;
80
+ alPasarUnSegundo?: (quedan: number) => void;
81
+ dormir?: (ms: number) => Promise<void>;
82
+ }): Promise<boolean>;
@@ -79,3 +79,37 @@ export function mensajeDeEspera(segundos) {
79
79
  "No llamo al motor porque la respuesta seria la misma y cada intento puede alargar la espera. " +
80
80
  "No hace falta que hagas nada: el hueco se libera solo.");
81
81
  }
82
+ /**
83
+ * Espera a que se libere el hueco, en vez de devolverle la pared al usuario.
84
+ *
85
+ * DE DONDE SALE
86
+ *
87
+ * La misma sesion, un paso mas alla. Ya no se gastaba una peticion por intento
88
+ * -eso lo arreglo lo de arriba-, pero el usuario seguia atrapado:
89
+ *
90
+ * > sigue
91
+ * ⏸ Faltan 144s.
92
+ * > sigoe
93
+ * ⏸ Faltan 133s.
94
+ *
95
+ * Tecleando contra una pared que el programa sabia medir. Si sabemos que faltan
96
+ * 133 segundos, esperarlos es trabajo del programa, no del que paga. Se avisa,
97
+ * se cuenta hacia atras y se sigue solo; con Esc se deja.
98
+ *
99
+ * Vuelve `true` si ya hay hueco y `false` si el usuario corto.
100
+ */
101
+ export async function esperarAlHueco(opciones) {
102
+ const { signal, alPasarUnSegundo } = opciones;
103
+ const dormir = opciones.dormir || ((ms) => new Promise((r) => setTimeout(r, ms)));
104
+ // Se relee en cada vuelta en vez de contar desde un numero fijo: si algo
105
+ // libera los huecos antes -otra respuesta del motor-, se sale en el acto.
106
+ let quedan = segundosDeEspera();
107
+ while (quedan > 0) {
108
+ if (signal?.aborted)
109
+ return false;
110
+ alPasarUnSegundo?.(quedan);
111
+ await dormir(1000);
112
+ quedan = segundosDeEspera();
113
+ }
114
+ return !signal?.aborted;
115
+ }
@@ -8,8 +8,9 @@ import { CONTEXT_WINDOW, MAX_OUTPUT_TOKENS, COMPACT_THRESHOLD_TOKENS, } from "..
8
8
  import { withRetry, isContextOverflow, esDemasiadasEnCurso } from "./retry.js";
9
9
  import { canRunInParallel } from "../tools/safety.js";
10
10
  import { createEngineClient, explicarErrorDelMotor, motivoDeParadaDelError, } from "../config/engine.js";
11
- import { anotarHuecosOcupados, liberarHuecos, mensajeDeEspera, segundosDeEspera } from "./huecos.js";
11
+ import { anotarHuecosOcupados, esperarAlHueco, liberarHuecos, mensajeDeEspera, segundosDeEspera, } from "./huecos.js";
12
12
  import { MarkdownStream } from "../ui/markdownStream.js";
13
+ import { apuntarCorte } from "./cortes.js";
13
14
  import { checkQuota, recordUsage, mensajeDeCorte, quotaBadge } from "../config/quota.js";
14
15
  import { ReasoningStream } from "../ui/reasoningStream.js";
15
16
  import { PermissionManager, commandSignature, isDangerousCommand } from "../config/permissions.js";
@@ -461,9 +462,43 @@ export class AgentLoop {
461
462
  // llamada es justo la que puede refrescar la reserva y alargar la espera.
462
463
  const espera = segundosDeEspera();
463
464
  if (espera > 0) {
465
+ // ESPERAR ES TRABAJO DEL PROGRAMA, NO DEL QUE PAGA
466
+ //
467
+ // Antes esto devolvia el aviso y cortaba el turno. El usuario escribia
468
+ // "sigue", se llevaba "faltan 144s", escribia otra vez y se llevaba
469
+ // "faltan 133s": tecleando contra una pared que el programa sabia medir.
470
+ // Si sabemos cuanto falta, se espera y se sigue solo. Con Esc se deja.
464
471
  const aviso = mensajeDeEspera(espera);
465
- salida.linea(chalk.yellow(`\n⏸ ${aviso}\n`));
466
- return parada("ocupado", aviso);
472
+ // SOLO SE ESPERA SI HAY ALGUIEN DELANTE.
473
+ //
474
+ // Esperar tres minutos es una pausa cuando se ve la cuenta atras y se
475
+ // puede cortar con Esc. Sin terminal -CI, `chocolatito "haz X"`, una
476
+ // tuberia- es un cuelgue mudo de tres minutos que nadie puede parar, y
477
+ // ademas ahi no hay nadie a quien ahorrarle el teclear. Se dice y se
478
+ // corta, como antes.
479
+ if (!process.stdin.isTTY) {
480
+ salida.linea(chalk.yellow(`\n⏸ ${aviso}\n`));
481
+ return parada("ocupado", aviso);
482
+ }
483
+ salida.linea(chalk.yellow(`\n⏸ ${aviso}`));
484
+ salida.linea(chalk.gray(" Espero y sigo solo cuando se libere. Esc para dejarlo.\n"));
485
+ const vigilante = new InterruptWatcher();
486
+ const senal = vigilante.start();
487
+ const spinnerEspera = new DynamicSpinner();
488
+ spinnerEspera.start();
489
+ const hayHueco = await esperarAlHueco({
490
+ signal: senal,
491
+ alPasarUnSegundo: (quedan) => {
492
+ spinnerEspera.setPrefijo(`Esperando hueco de la licencia (quedan ${quedan}s)`);
493
+ },
494
+ });
495
+ spinnerEspera.stop();
496
+ vigilante.stop();
497
+ if (!hayHueco) {
498
+ salida.linea(chalk.yellow("\n⏸ Interrumpido. Dime como seguir."));
499
+ return parada("interrumpido");
500
+ }
501
+ salida.linea(chalk.green(" ✔ Hueco libre. Sigo.\n"));
467
502
  }
468
503
  // Cuota ANTES de gastar. Si no queda margen, se dice y no se llama al motor:
469
504
  // avisar despues de haber gastado no sirve de nada.
@@ -596,6 +631,10 @@ export class AgentLoop {
596
631
  * respuesta de un motor que no mande esa marca.
597
632
  */
598
633
  let llegoElFinal = false;
634
+ // Para la caja negra: sin estos dos numeros, un corte no se puede
635
+ // comparar con el siguiente, y comparar es lo unico que da un patron.
636
+ let trozos = 0;
637
+ const empezoEn = Date.now();
599
638
  const rawToolCalls = [];
600
639
  const reasoningStream = new ReasoningStream();
601
640
  // Por `salida`, no por stdout: mientras se escribe la respuesta el spinner
@@ -607,6 +646,7 @@ export class AgentLoop {
607
646
  // fijo, asi que la respuesta sale por encima sin taparlo y el contador
608
647
  // de segundos sigue a la vista mientras el modelo habla, que es
609
648
  // exactamente cuando uno quiere saber cuanto lleva.
649
+ trozos++;
610
650
  const delta = chunk.choices?.[0]?.delta;
611
651
  // Handle real-time reasoning stream
612
652
  if (delta?.reasoning_content) {
@@ -680,6 +720,23 @@ export class AgentLoop {
680
720
  }
681
721
  if (esDemasiadasEnCurso(streamErr))
682
722
  anotarHuecosOcupados(streamErr?.error?.retry_after_s);
723
+ if (String(streamErr?.error?.code ?? "") === "motor_corto_la_respuesta") {
724
+ apuntarCorte({
725
+ modelo: this.currentEspectro.underlyingModel,
726
+ ms: Date.now() - empezoEn,
727
+ trozos,
728
+ bytesTexto: Buffer.byteLength(content, "utf8"),
729
+ bytesRazonamiento: Buffer.byteLength(reasoning, "utf8"),
730
+ aviso: "motor",
731
+ });
732
+ // Lo que llego es trabajo util aunque este a medias.
733
+ if (content.trim()) {
734
+ this.messages.push({
735
+ role: "assistant",
736
+ content: `${content}\n\n[Aqui se corto: el motor cerro la respuesta sin terminarla.]`,
737
+ });
738
+ }
739
+ }
683
740
  const claroStream = explicarErrorDelMotor(streamErr);
684
741
  if (claroStream) {
685
742
  salida.linea(chalk.yellow(`
@@ -742,6 +799,18 @@ export class AgentLoop {
742
799
  // porque la conexion estaba bien.
743
800
  if (!llegoElFinal) {
744
801
  watcher.stop();
802
+ // SE CIERRA LA PETICION MUERTA, Y ESTO NO ES LIMPIEZA: ES EL HUECO.
803
+ //
804
+ // El cuerpo se corto, pero por lo que respecta al proxy la peticion
805
+ // sigue en curso y sigue ocupando uno de los cuatro huecos de la
806
+ // licencia hasta que ALGO la cierre. Por eso el mensaje del proxy dice
807
+ // "si acabas de interrumpir una, el hueco se libera solo en 206s": son
808
+ // tres minutos y medio de servicio perdido por una conexion que ya
809
+ // estaba muerta de este lado. Abortando aqui se suelta en el momento.
810
+ try {
811
+ stream?.controller?.abort();
812
+ }
813
+ catch { }
745
814
  // Lo que llego se guarda, pero MARCADO. Sin la marca, en el turno
746
815
  // siguiente el modelo lee su propia frase a medias como algo que decidio
747
816
  // dejar asi, y responde a un texto que nadie escribio.
@@ -761,11 +830,23 @@ export class AgentLoop {
761
830
  : reasoning.trim()
762
831
  ? "Se corto antes de escribir nada: solo llego el razonamiento."
763
832
  : "Se corto antes de que llegara nada.";
833
+ // Se apunta antes de hablar: si el usuario vuelve a teclear, el aviso se
834
+ // pierde con el scroll, pero el renglon se queda.
835
+ const registro = apuntarCorte({
836
+ modelo: this.currentEspectro.underlyingModel,
837
+ ms: Date.now() - empezoEn,
838
+ trozos,
839
+ bytesTexto: Buffer.byteLength(content, "utf8"),
840
+ bytesRazonamiento: Buffer.byteLength(reasoning, "utf8"),
841
+ aviso: "mudo",
842
+ });
764
843
  const aviso = "La respuesta se corto: la conexion con el motor se cerro sin decir por que (no llego " +
765
844
  `ni el motivo de fin ni el recuento del gasto). ${hastaDonde} ` +
766
845
  "No lo reintento solo porque cada intento ocupa uno de los cuatro huecos de tu licencia " +
767
846
  "durante varios minutos; vuelve a pedirmelo y sigo.";
768
847
  salida.linea(chalk.yellow(`\n ⚠ ${aviso}\n`));
848
+ if (registro)
849
+ salida.linea(chalk.gray(` (apuntado en ${registro} — /cortes para verlos)\n`));
769
850
  return parada("error-motor", aviso);
770
851
  }
771
852
  // El modelo se quedo sin presupuesto de salida y no llego a pedir nada.
@@ -125,6 +125,13 @@ export function explicarErrorDelMotor(err) {
125
125
  cuando +
126
126
  " No es un problema de tu conexion: si interrumpiste una tarea, su hueco se libera solo.");
127
127
  }
128
+ // El proxy avisa cuando el motor cierra la respuesta sin terminarla. Es el
129
+ // unico caso en el que se puede decir de QUIEN fue el corte: el cliente, desde
130
+ // fuera, solo ve un cuerpo que se acaba, igual que uno que termino bien.
131
+ if (String(err?.error?.code ?? "") === "motor_corto_la_respuesta") {
132
+ return ((mensaje || "El motor cerro la respuesta antes de terminarla.") +
133
+ " Lo que llego se queda arriba; vuelve a pedirlo y normalmente sale a la primera.");
134
+ }
128
135
  if (tipo === "cuota") {
129
136
  return mensaje || "Has agotado la cuota de uso incluida en tu suscripción.";
130
137
  }
package/dist/index.js CHANGED
@@ -33,6 +33,7 @@ import { SessionManager } from "./sessions/manager.js";
33
33
  import { prepararRestauracion, formatearOpcionesDeSesion } from "./sessions/resume.js";
34
34
  import { auditGitDiff, formatAuditReport } from "./tools/gitAudit.js";
35
35
  import { instalarKit, listarKits, quitarKit, crearKit } from "./kits/manager.js";
36
+ import { leerCortes, rutaDelRegistro } from "./agent/cortes.js";
36
37
  import { globalUndoManager } from "./agent/undoManager.js";
37
38
  import { setAutoApprove } from "./ui/permissionPrompt.js";
38
39
  import { onModeChange, getMode, modeAnnouncement } from "./ui/modes.js";
@@ -616,6 +617,23 @@ ${sessionData}
616
617
  console.log("");
617
618
  continue;
618
619
  }
620
+ if (trimmed === "/cortes") {
621
+ const lineas = leerCortes(20);
622
+ console.log(chalk.bold.hex("#D97757")("\n✂ RESPUESTAS QUE SE CORTARON"));
623
+ if (lineas.length === 0) {
624
+ console.log(chalk.gray(" Ninguna. Bien.\n"));
625
+ }
626
+ else {
627
+ console.log(chalk.gray(` ${rutaDelRegistro()}\n`));
628
+ for (const l of lineas)
629
+ console.log(" " + chalk.gray(l));
630
+ console.log(chalk.gray("\n aviso=motor -> lo dijo el proxy: el corte viene del motor de arriba." +
631
+ "\n aviso=mudo -> nadie dijo nada: se cayo la conexion o el proxy por el camino." +
632
+ "\n Si ms y trozos salen casi iguales en todos, algo corta por tiempo o tamano;" +
633
+ "\n si salen dispares, es el motor y va a rachas.\n"));
634
+ }
635
+ continue;
636
+ }
619
637
  if (trimmed === "/kit" || trimmed.startsWith("/kit ")) {
620
638
  const partes = trimmed.split(/s+/).slice(1);
621
639
  const accion = (partes[0] || "").toLowerCase();
@@ -32,6 +32,7 @@ export const SLASH_COMMANDS = [
32
32
  { name: "/skills", description: "Ver skills disponibles y cómo usarlos" },
33
33
  { name: "/kit", description: "Montar tu Chocolatito: instalar, crear o quitar kits (skills + reglas + MCP)", args: "[instalar|crear|quitar] [nombre]" },
34
34
  { name: "/aprende", description: "Guardar lo que acabamos de hacer como skill reutilizable", args: "[en pocas palabras]" },
35
+ { name: "/cortes", description: "Ver las respuestas que se cortaron y de dónde vino el corte" },
35
36
  { name: "/memory", description: "Consultar la memoria persistente transversal (~/.chocolatito/memory/)" },
36
37
  { name: "/effort", description: "Regular potencia del motor (1.0, 1.5, 2.0, 2.5)", args: "[nivel]" },
37
38
  { name: "/goal", description: "Ejecutar meta compleja autónoma con auto-corrección", args: "<meta>" },
@@ -7,6 +7,12 @@ export declare class DynamicSpinner {
7
7
  private prefix;
8
8
  tokenCount: number;
9
9
  constructor(prefix?: string);
10
+ /**
11
+ * Cambia el texto mientras gira. Hace falta para las esperas con cuenta
12
+ * atras: el prefijo se fijaba al arrancar y el numero de segundos que faltan
13
+ * cambia cada vuelta.
14
+ */
15
+ setPrefijo(prefijo: string): void;
10
16
  start(initialPrefix?: string): void;
11
17
  private tick;
12
18
  /**
@@ -42,6 +42,14 @@ export class DynamicSpinner {
42
42
  constructor(prefix = "") {
43
43
  this.prefix = prefix;
44
44
  }
45
+ /**
46
+ * Cambia el texto mientras gira. Hace falta para las esperas con cuenta
47
+ * atras: el prefijo se fijaba al arrancar y el numero de segundos que faltan
48
+ * cambia cada vuelta.
49
+ */
50
+ setPrefijo(prefijo) {
51
+ this.prefix = prefijo;
52
+ }
45
53
  start(initialPrefix) {
46
54
  if (this.isRunning)
47
55
  return;
@@ -2,7 +2,7 @@
2
2
  "manifest_version": 3,
3
3
  "minimum_chrome_version": "102",
4
4
  "name": "Chocolatito Code",
5
- "version": "1.6.11",
5
+ "version": "1.6.13",
6
6
  "description": "Deja que Chocolatito Code trabaje dentro de tu Chrome, en segundo plano y con tus sesiones ya iniciadas.",
7
7
  "permissions": [
8
8
  "tabs",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chocolatito-code",
3
- "version": "1.6.11",
3
+ "version": "1.6.13",
4
4
  "description": "Agente autónomo de programación para la terminal, con control real del ordenador y del navegador. Desarrollado por Chocolatito.",
5
5
  "type": "module",
6
6
  "bin": {