@adhdev/session-host-core 1.0.58-rc.9 → 1.0.58-rc.90

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/index.d.mts CHANGED
@@ -533,7 +533,27 @@ interface SessionHostEndpointOptions {
533
533
  platform?: NodeJS.Platform;
534
534
  }
535
535
  declare function getDefaultSessionHostEndpoint(appName?: string, options?: SessionHostEndpointOptions): SessionHostEndpoint;
536
- declare function createLineParser(onEnvelope: (envelope: SessionHostWireEnvelope) => void): (chunk: Buffer | string) => void;
536
+ /**
537
+ * UTF8-CHUNK-BOUNDARY: decode the socket stream, never the individual chunk.
538
+ *
539
+ * This used to call `chunk.toString()` on every Buffer independently. A socket
540
+ * hands us arbitrary byte slices — a multi-byte UTF-8 sequence straddling two
541
+ * chunks gets decoded as two truncated sequences, and each half becomes U+FFFD.
542
+ * The damage is silent by construction: the replacement character is valid JSON
543
+ * string content, so the envelope still parses and only the *value* is wrong.
544
+ * A pasted 11,994-char Korean prompt lost exactly one character that way — the
545
+ * `도` beginning at wire offset 8190, split across the 8192-byte chunk boundary,
546
+ * arrived as `�착`.
547
+ *
548
+ * StringDecoder holds an incomplete trailing sequence back until the bytes that
549
+ * finish it arrive, which is the only correct way to turn a byte stream into
550
+ * text. String chunks bypass it: they are already decoded, and feeding them
551
+ * through `Buffer.from` would re-encode text the caller never asked us to touch.
552
+ */
553
+ declare function createLineParser(onEnvelope: (envelope: SessionHostWireEnvelope) => void): {
554
+ (chunk: Buffer | string): void;
555
+ end(): string;
556
+ };
537
557
  interface SessionHostClientOptions {
538
558
  endpoint?: SessionHostEndpoint;
539
559
  appName?: string;
@@ -551,6 +571,16 @@ interface SessionHostDisconnectInfo {
551
571
  /** In-flight requests abandoned by this disconnect (they were rejected). */
552
572
  pendingRequests: number;
553
573
  error?: Error;
574
+ /**
575
+ * Bytes discarded from a newline-unterminated frame at EOF (0 when the peer
576
+ * hung up on a clean frame boundary, which is the normal case).
577
+ *
578
+ * Non-zero means the peer was cut off MID-WRITE — diagnostic context that
579
+ * distinguishes "the host exited" from "the host died with a frame in
580
+ * flight". Before the parser's `end()` was reachable this was unknowable:
581
+ * the bytes were dropped with the decoder and no counter existed.
582
+ */
583
+ droppedTailBytes: number;
554
584
  }
555
585
  declare class SessionHostClient {
556
586
  readonly endpoint: SessionHostEndpoint;
package/dist/index.d.ts CHANGED
@@ -533,7 +533,27 @@ interface SessionHostEndpointOptions {
533
533
  platform?: NodeJS.Platform;
534
534
  }
535
535
  declare function getDefaultSessionHostEndpoint(appName?: string, options?: SessionHostEndpointOptions): SessionHostEndpoint;
536
- declare function createLineParser(onEnvelope: (envelope: SessionHostWireEnvelope) => void): (chunk: Buffer | string) => void;
536
+ /**
537
+ * UTF8-CHUNK-BOUNDARY: decode the socket stream, never the individual chunk.
538
+ *
539
+ * This used to call `chunk.toString()` on every Buffer independently. A socket
540
+ * hands us arbitrary byte slices — a multi-byte UTF-8 sequence straddling two
541
+ * chunks gets decoded as two truncated sequences, and each half becomes U+FFFD.
542
+ * The damage is silent by construction: the replacement character is valid JSON
543
+ * string content, so the envelope still parses and only the *value* is wrong.
544
+ * A pasted 11,994-char Korean prompt lost exactly one character that way — the
545
+ * `도` beginning at wire offset 8190, split across the 8192-byte chunk boundary,
546
+ * arrived as `�착`.
547
+ *
548
+ * StringDecoder holds an incomplete trailing sequence back until the bytes that
549
+ * finish it arrive, which is the only correct way to turn a byte stream into
550
+ * text. String chunks bypass it: they are already decoded, and feeding them
551
+ * through `Buffer.from` would re-encode text the caller never asked us to touch.
552
+ */
553
+ declare function createLineParser(onEnvelope: (envelope: SessionHostWireEnvelope) => void): {
554
+ (chunk: Buffer | string): void;
555
+ end(): string;
556
+ };
537
557
  interface SessionHostClientOptions {
538
558
  endpoint?: SessionHostEndpoint;
539
559
  appName?: string;
@@ -551,6 +571,16 @@ interface SessionHostDisconnectInfo {
551
571
  /** In-flight requests abandoned by this disconnect (they were rejected). */
552
572
  pendingRequests: number;
553
573
  error?: Error;
574
+ /**
575
+ * Bytes discarded from a newline-unterminated frame at EOF (0 when the peer
576
+ * hung up on a clean frame boundary, which is the normal case).
577
+ *
578
+ * Non-zero means the peer was cut off MID-WRITE — diagnostic context that
579
+ * distinguishes "the host exited" from "the host died with a frame in
580
+ * flight". Before the parser's `end()` was reachable this was unknowable:
581
+ * the bytes were dropped with the decoder and no counter existed.
582
+ */
583
+ droppedTailBytes: number;
554
584
  }
555
585
  declare class SessionHostClient {
556
586
  readonly endpoint: SessionHostEndpoint;
package/dist/index.js CHANGED
@@ -580,6 +580,7 @@ var SessionHostRegistry = class {
580
580
  var os = __toESM(require("os"));
581
581
  var path2 = __toESM(require("path"));
582
582
  var net = __toESM(require("net"));
583
+ var import_string_decoder = require("string_decoder");
583
584
  function generateUUID2() {
584
585
  if (typeof crypto !== "undefined" && crypto.randomUUID) {
585
586
  return crypto.randomUUID();
@@ -614,8 +615,9 @@ function serializeEnvelope(envelope) {
614
615
  }
615
616
  function createLineParser(onEnvelope) {
616
617
  let buffer = "";
617
- return (chunk) => {
618
- buffer += chunk.toString();
618
+ const decoder = new import_string_decoder.StringDecoder("utf8");
619
+ const parser = (chunk) => {
620
+ buffer += typeof chunk === "string" ? chunk : decoder.write(chunk);
619
621
  let newlineIndex = buffer.indexOf("\n");
620
622
  while (newlineIndex >= 0) {
621
623
  const rawLine = buffer.slice(0, newlineIndex).trim();
@@ -626,6 +628,12 @@ function createLineParser(onEnvelope) {
626
628
  newlineIndex = buffer.indexOf("\n");
627
629
  }
628
630
  };
631
+ parser.end = () => {
632
+ const remainder = buffer + decoder.end();
633
+ buffer = "";
634
+ return remainder;
635
+ };
636
+ return parser;
629
637
  }
630
638
  var establishedSockets = /* @__PURE__ */ new WeakSet();
631
639
  var SessionHostClient = class {
@@ -657,7 +665,7 @@ var SessionHostClient = class {
657
665
  this.disconnectListeners.delete(listener);
658
666
  };
659
667
  }
660
- handleDisconnect(socket, reason, error) {
668
+ handleDisconnect(socket, reason, error, droppedTailBytes = 0) {
661
669
  if (this.disconnectedSockets.has(socket)) return;
662
670
  this.disconnectedSockets.add(socket);
663
671
  const wasEstablished = establishedSockets.has(socket);
@@ -689,7 +697,8 @@ var SessionHostClient = class {
689
697
  reason,
690
698
  endpointPath: this.endpoint.path,
691
699
  pendingRequests,
692
- error
700
+ error,
701
+ droppedTailBytes
693
702
  };
694
703
  for (const listener of this.disconnectListeners) {
695
704
  try {
@@ -709,7 +718,7 @@ var SessionHostClient = class {
709
718
  }
710
719
  const socket = net.createConnection(this.endpoint.path);
711
720
  this.socket = socket;
712
- socket.on("data", createLineParser((envelope) => {
721
+ const parser = createLineParser((envelope) => {
713
722
  if (envelope.kind === "response") {
714
723
  const waiter = this.requestWaiters.get(envelope.requestId);
715
724
  if (waiter) {
@@ -721,15 +730,24 @@ var SessionHostClient = class {
721
730
  if (envelope.kind === "event") {
722
731
  for (const listener of this.eventListeners) listener(envelope.event);
723
732
  }
724
- }));
733
+ });
734
+ socket.on("data", parser);
735
+ const flushParser = () => {
736
+ try {
737
+ return parser.end();
738
+ } catch {
739
+ return "";
740
+ }
741
+ };
725
742
  socket.on("error", (error) => {
726
- this.handleDisconnect(socket, "error", error);
743
+ const remainder = flushParser();
744
+ this.handleDisconnect(socket, "error", error, remainder.length);
727
745
  });
728
746
  socket.on("close", () => {
729
- this.handleDisconnect(socket, "closed");
747
+ this.handleDisconnect(socket, "closed", void 0, flushParser().length);
730
748
  });
731
749
  socket.on("end", () => {
732
- this.handleDisconnect(socket, "ended");
750
+ this.handleDisconnect(socket, "ended", void 0, flushParser().length);
733
751
  });
734
752
  await new Promise((resolve3, reject) => {
735
753
  socket.once("connect", () => {
@@ -867,6 +885,8 @@ function sanitizeSpawnEnv(baseEnv, overrides) {
867
885
  delete env.CODEX_SANDBOX_NETWORK_DISABLED;
868
886
  delete env.NO_COLOR;
869
887
  delete env.COLOR;
888
+ delete env.COLUMNS;
889
+ delete env.LINES;
870
890
  delete env.CLAUDECODE;
871
891
  delete env.CLAUDE_CODE_CHILD_SESSION;
872
892
  delete env.CLAUDE_CODE_ENTRYPOINT;