@adhdev/session-host-core 1.0.60-rc.9 → 1.0.61-rc.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/index.mjs CHANGED
@@ -117,13 +117,124 @@ var SessionRingBuffer = class {
117
117
  this.trim();
118
118
  }
119
119
  trim() {
120
+ let evicted = false;
120
121
  while (this.totalBytes > this.maxBytes && this.chunks.length > 1) {
121
122
  const removed = this.chunks.shift();
122
123
  if (!removed) break;
123
124
  this.totalBytes -= removed.bytes;
125
+ evicted = true;
124
126
  }
127
+ if (evicted) this.healHead();
128
+ }
129
+ /**
130
+ * TRIM-BOUNDARY repair. Eviction drops whole chunks, and a chunk boundary is
131
+ * a PTY read boundary — an arbitrary byte offset with no relationship to the
132
+ * structure of the stream. So the chunk that becomes the new oldest can begin
133
+ * partway through something the sender wrote atomically, and `snapshot()`
134
+ * joins from exactly there.
135
+ *
136
+ * The consumer is the browser terminal: "Load older terminal output" asks
137
+ * with `sinceSeq: 0`, which makes the daemon skip the emulator viewport and
138
+ * hand this raw text straight to xterm (see `mergeRuntimeSnapshot`). xterm
139
+ * then parses a stream that starts mid-token, which is how the reported
140
+ * screenshot got orphaned `.` and `5` glyphs floating above the output and a
141
+ * large blank band at the top:
142
+ *
143
+ * drop 5 chars -> "[HClaude Code v2.1.220…" the CSI introducer is gone,
144
+ * so `[H` prints literally
145
+ * drop 12 chars -> "2mClaude Code v2.1.220…" half an SGR prints literally
146
+ * drop 46 chars -> the leading `\x1b[2J\x1b[H` never arrives, so the screen
147
+ * is never cleared/homed and row placement collapses
148
+ * byte cut -> a torn 3-byte Hangul sequence decodes to U+FFFD
149
+ *
150
+ * So walk the head of the new oldest chunk forward to the first offset that
151
+ * is safe to start parsing at, and drop the partial prefix. Losing a few
152
+ * bytes of already-evicted context is strictly better than injecting literal
153
+ * garbage into the viewport.
154
+ *
155
+ * Two independent boundary classes have to be handled, and neither subsumes
156
+ * the other:
157
+ *
158
+ * 1. Character encoding. Chunks are JS strings, so a torn multi-byte UTF-8
159
+ * sequence has already decayed into U+FFFD (or, for astral characters, a
160
+ * lone surrogate) by the time it gets here. This mirrors the protection
161
+ * `createLineParser` grew for the IPC socket path in
162
+ * `ipc-line-parser-utf8.test.ts`; that layer can hold bytes back and
163
+ * re-join them because it owns both sides of the split, whereas here the
164
+ * other half is already gone, so dropping is the only repair available.
165
+ * 2. Escape sequences. A CSI/OSC/SS3 can be cut anywhere, and unlike the
166
+ * encoding case the leftover bytes are all perfectly printable — which is
167
+ * precisely why the corruption is visible rather than silent.
168
+ */
169
+ healHead() {
170
+ const head = this.chunks[0];
171
+ if (!head) return;
172
+ const repaired = stripDanglingPrefix(head.data);
173
+ if (repaired === head.data) return;
174
+ const bytes = Buffer.byteLength(repaired, "utf8");
175
+ this.totalBytes -= head.bytes - bytes;
176
+ head.data = repaired;
177
+ head.bytes = bytes;
125
178
  }
126
179
  };
180
+ var CSI_FINAL = /[\x40-\x7e]/;
181
+ var CSI_COMMON_FINAL = /[ABCDEFGHJKSTLMPX@mnchlsurdfgqit]/;
182
+ function stripDanglingPrefix(text) {
183
+ let i = 0;
184
+ while (i < text.length) {
185
+ const code = text.charCodeAt(i);
186
+ if (code === 65533) {
187
+ i += 1;
188
+ continue;
189
+ }
190
+ if (code >= 55296 && code <= 56319) {
191
+ const next = text.charCodeAt(i + 1);
192
+ if (next >= 56320 && next <= 57343) break;
193
+ i += 1;
194
+ continue;
195
+ }
196
+ if (code >= 56320 && code <= 57343) {
197
+ i += 1;
198
+ continue;
199
+ }
200
+ break;
201
+ }
202
+ const rest = text.slice(i);
203
+ const danglingLength = danglingEscapeTailLength(rest);
204
+ return danglingLength > 0 ? rest.slice(danglingLength) : rest;
205
+ }
206
+ function danglingEscapeTailLength(text) {
207
+ if (!text || text.charCodeAt(0) === 27) return 0;
208
+ let i = 0;
209
+ const hasIntroducer = text[0] === "[" || text[0] === "]";
210
+ if (hasIntroducer) i = 1;
211
+ const start = i;
212
+ while (i < text.length && /[0-9;?:<=>]/.test(text[i])) i += 1;
213
+ if (i >= text.length) return 0;
214
+ if (text[0] === "]") {
215
+ const bel = text.indexOf("\x07");
216
+ if (bel >= 0) return bel + 1;
217
+ return 0;
218
+ }
219
+ if (!CSI_FINAL.test(text[i])) return 0;
220
+ if (!hasIntroducer) {
221
+ if (i === start) return 0;
222
+ if (!CSI_COMMON_FINAL.test(text[i])) return 0;
223
+ }
224
+ return i + 1;
225
+ }
226
+
227
+ // src/uuid.ts
228
+ function generateUUID() {
229
+ if (typeof crypto !== "undefined" && crypto.randomUUID) {
230
+ return crypto.randomUUID();
231
+ }
232
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
233
+ const r = Math.random() * 16 | 0;
234
+ const v = c === "x" ? r : r & 3 | 8;
235
+ return v.toString(16);
236
+ });
237
+ }
127
238
 
128
239
  // src/defaults.ts
129
240
  var DEFAULT_SESSION_HOST_COLS = 80;
@@ -285,16 +396,6 @@ function formatRuntimeOwner(record) {
285
396
  }
286
397
 
287
398
  // src/registry.ts
288
- function generateUUID() {
289
- if (typeof crypto !== "undefined" && crypto.randomUUID) {
290
- return crypto.randomUUID();
291
- }
292
- return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
293
- const r = Math.random() * 16 | 0;
294
- const v = c === "x" ? r : r & 3 | 8;
295
- return v.toString(16);
296
- });
297
- }
298
399
  var SessionHostRegistry = class {
299
400
  sessions = /* @__PURE__ */ new Map();
300
401
  createSession(payload) {
@@ -507,26 +608,27 @@ var SessionHostRegistry = class {
507
608
  writeOwner: record.writeOwner ? { ...record.writeOwner } : null,
508
609
  attachedClients: record.attachedClients.map((client) => ({ ...client })),
509
610
  buffer: { ...record.buffer },
510
- meta: { ...record.meta }
611
+ // Deep: meta carries nested records (e.g. the daemon's `launchRecord`,
612
+ // wiring-unification Phase E), and a caller mutating a returned record
613
+ // must never reach into the registry's own copy.
614
+ meta: cloneMeta(record.meta)
511
615
  };
512
616
  }
513
617
  };
618
+ function cloneMeta(meta) {
619
+ if (!meta) return {};
620
+ try {
621
+ return structuredClone(meta);
622
+ } catch {
623
+ return { ...meta };
624
+ }
625
+ }
514
626
 
515
627
  // src/ipc.ts
516
628
  import * as os from "os";
517
629
  import * as path2 from "path";
518
630
  import * as net from "net";
519
631
  import { StringDecoder } from "string_decoder";
520
- function generateUUID2() {
521
- if (typeof crypto !== "undefined" && crypto.randomUUID) {
522
- return crypto.randomUUID();
523
- }
524
- return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
525
- const r = Math.random() * 16 | 0;
526
- const v = c === "x" ? r : r & 3 | 8;
527
- return v.toString(16);
528
- });
529
- }
530
632
  function getDefaultSessionHostEndpoint(appName = "adhdev", options = {}) {
531
633
  const rawKey = typeof options.ipcKey === "string" ? options.ipcKey.trim() : "";
532
634
  if (rawKey && !/^[0-9a-f]{12}$/.test(rawKey)) {
@@ -702,7 +804,7 @@ var SessionHostClient = class {
702
804
  async request(request) {
703
805
  await this.connect();
704
806
  if (!this.socket) throw new Error("Session host socket unavailable");
705
- const requestId = generateUUID2();
807
+ const requestId = generateUUID();
706
808
  const envelope = {
707
809
  kind: "request",
708
810
  requestId,
@@ -896,6 +998,9 @@ function createSessionHostControlPlane(transport) {
896
998
  },
897
999
  releaseWrite(payload) {
898
1000
  return transport.request("release_write", payload);
1001
+ },
1002
+ getSnapshot(sessionId, sinceSeq) {
1003
+ return transport.request("get_snapshot", { sessionId, sinceSeq });
899
1004
  }
900
1005
  };
901
1006
  }