@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.d.mts +60 -3
- package/dist/index.d.ts +60 -3
- package/dist/index.js +127 -22
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +127 -22
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/buffer.ts +161 -0
- package/src/control-plane.ts +23 -2
- package/src/index.ts +1 -1
- package/src/ipc.ts +1 -11
- package/src/registry.ts +15 -11
- package/src/uuid.ts +11 -0
package/dist/index.d.mts
CHANGED
|
@@ -456,6 +456,47 @@ declare class SessionRingBuffer {
|
|
|
456
456
|
text: string;
|
|
457
457
|
}): void;
|
|
458
458
|
private trim;
|
|
459
|
+
/**
|
|
460
|
+
* TRIM-BOUNDARY repair. Eviction drops whole chunks, and a chunk boundary is
|
|
461
|
+
* a PTY read boundary — an arbitrary byte offset with no relationship to the
|
|
462
|
+
* structure of the stream. So the chunk that becomes the new oldest can begin
|
|
463
|
+
* partway through something the sender wrote atomically, and `snapshot()`
|
|
464
|
+
* joins from exactly there.
|
|
465
|
+
*
|
|
466
|
+
* The consumer is the browser terminal: "Load older terminal output" asks
|
|
467
|
+
* with `sinceSeq: 0`, which makes the daemon skip the emulator viewport and
|
|
468
|
+
* hand this raw text straight to xterm (see `mergeRuntimeSnapshot`). xterm
|
|
469
|
+
* then parses a stream that starts mid-token, which is how the reported
|
|
470
|
+
* screenshot got orphaned `.` and `5` glyphs floating above the output and a
|
|
471
|
+
* large blank band at the top:
|
|
472
|
+
*
|
|
473
|
+
* drop 5 chars -> "[HClaude Code v2.1.220…" the CSI introducer is gone,
|
|
474
|
+
* so `[H` prints literally
|
|
475
|
+
* drop 12 chars -> "2mClaude Code v2.1.220…" half an SGR prints literally
|
|
476
|
+
* drop 46 chars -> the leading `\x1b[2J\x1b[H` never arrives, so the screen
|
|
477
|
+
* is never cleared/homed and row placement collapses
|
|
478
|
+
* byte cut -> a torn 3-byte Hangul sequence decodes to U+FFFD
|
|
479
|
+
*
|
|
480
|
+
* So walk the head of the new oldest chunk forward to the first offset that
|
|
481
|
+
* is safe to start parsing at, and drop the partial prefix. Losing a few
|
|
482
|
+
* bytes of already-evicted context is strictly better than injecting literal
|
|
483
|
+
* garbage into the viewport.
|
|
484
|
+
*
|
|
485
|
+
* Two independent boundary classes have to be handled, and neither subsumes
|
|
486
|
+
* the other:
|
|
487
|
+
*
|
|
488
|
+
* 1. Character encoding. Chunks are JS strings, so a torn multi-byte UTF-8
|
|
489
|
+
* sequence has already decayed into U+FFFD (or, for astral characters, a
|
|
490
|
+
* lone surrogate) by the time it gets here. This mirrors the protection
|
|
491
|
+
* `createLineParser` grew for the IPC socket path in
|
|
492
|
+
* `ipc-line-parser-utf8.test.ts`; that layer can hold bytes back and
|
|
493
|
+
* re-join them because it owns both sides of the split, whereas here the
|
|
494
|
+
* other half is already gone, so dropping is the only repair available.
|
|
495
|
+
* 2. Escape sequences. A CSI/OSC/SS3 can be cut anywhere, and unlike the
|
|
496
|
+
* encoding case the leftover bytes are all perfectly printable — which is
|
|
497
|
+
* precisely why the corruption is visible rather than silent.
|
|
498
|
+
*/
|
|
499
|
+
private healHead;
|
|
459
500
|
}
|
|
460
501
|
|
|
461
502
|
declare class SessionHostRegistry {
|
|
@@ -708,8 +749,16 @@ declare function applyTerminalColorEnv(env: Record<string, string>): void;
|
|
|
708
749
|
*/
|
|
709
750
|
declare function ensureNodePtySpawnHelperPermissions(logFn?: (msg: string) => void): void;
|
|
710
751
|
|
|
752
|
+
/** Response shape of the `get_snapshot` wire request — a PTY snapshot for a session. */
|
|
753
|
+
interface SessionHostSnapshot {
|
|
754
|
+
seq: number;
|
|
755
|
+
text: string;
|
|
756
|
+
truncated: boolean;
|
|
757
|
+
cols?: number;
|
|
758
|
+
rows?: number;
|
|
759
|
+
}
|
|
711
760
|
/**
|
|
712
|
-
* The
|
|
761
|
+
* The session-host control-plane surface shared by the cloud and
|
|
713
762
|
* standalone daemons. Both daemons used to carry a byte-for-byte copy of this
|
|
714
763
|
* dispatch table plus the identical throw strings; this is the single source of
|
|
715
764
|
* truth for that mapping. The wire `type` strings and error text here are the
|
|
@@ -729,6 +778,14 @@ interface SessionHostControlPlane {
|
|
|
729
778
|
pruneDuplicateSessions(payload?: PruneDuplicateSessionsPayload): Promise<SessionHostPruneDuplicatesResult>;
|
|
730
779
|
acquireWrite(payload: AcquireWritePayload): Promise<SessionHostRecord | null>;
|
|
731
780
|
releaseWrite(payload: ReleaseWritePayload): Promise<SessionHostRecord | null>;
|
|
781
|
+
/**
|
|
782
|
+
* A PTY snapshot for `sessionId` (optionally only the tail since `sinceSeq`),
|
|
783
|
+
* over wire type `get_snapshot`. Mirrors the shape daemon-core's local
|
|
784
|
+
* `SessionHostControlPlane` interface (`commands/router.ts`) and cloud's
|
|
785
|
+
* former `cloud-command-transports.ts` P2P-only `get_runtime_snapshot` case
|
|
786
|
+
* both already used against a raw `SessionHostClient`.
|
|
787
|
+
*/
|
|
788
|
+
getSnapshot(sessionId: string, sinceSeq?: number): Promise<SessionHostSnapshot | null>;
|
|
732
789
|
}
|
|
733
790
|
/**
|
|
734
791
|
* Minimal request transport the control-plane factory needs. Each daemon injects
|
|
@@ -745,11 +802,11 @@ interface SessionHostControlTransport {
|
|
|
745
802
|
request<T>(type: SessionHostRequestType, payload: Record<string, unknown>): Promise<T>;
|
|
746
803
|
}
|
|
747
804
|
/**
|
|
748
|
-
* Build the shared
|
|
805
|
+
* Build the shared 12-method control-plane over an injected request transport.
|
|
749
806
|
* The (type string, payload shape) pairs are verbatim what both daemons emitted
|
|
750
807
|
* previously — do not reword them without matching the session-host daemon's
|
|
751
808
|
* request handlers.
|
|
752
809
|
*/
|
|
753
810
|
declare function createSessionHostControlPlane(transport: SessionHostControlTransport): SessionHostControlPlane;
|
|
754
811
|
|
|
755
|
-
export { type AcquireWritePayload, type AttachSessionPayload, type ClearSessionBufferPayload, type CreateSessionPayload, DEFAULT_CONFIG_DIR_NAME, DEFAULT_SESSION_RING_BUFFER_MAX_BYTES, type DetachSessionPayload, type ForceDetachClientPayload, type GetHostDiagnosticsPayload, type GetSnapshotPayload, type GetTerminalSnapshotPayload, type PruneDuplicateSessionsPayload, type ReleaseWritePayload, type ResizeSessionPayload, type RestartSessionPayload, type ResumeSessionPayload, SESSION_HOST_SUPPORTED_REQUEST_TYPES, type SendInputPayload, type SendSignalPayload, type SessionAttachedClient, type SessionBufferSnapshot, type SessionClientType, type SessionHostCategory, SessionHostClient, type SessionHostClientOptions, type SessionHostControlPlane, type SessionHostControlTransport, type SessionHostDiagnostics, type SessionHostDisconnectInfo, type SessionHostDisconnectReason, type SessionHostDuplicateSessionGroup, type SessionHostEndpoint, type SessionHostEndpointOptions, type SessionHostEvent, type SessionHostEventEnvelope, type SessionHostLogEntry, type SessionHostPruneDuplicatesResult, type SessionHostRecord, SessionHostRegistry, type SessionHostRequest, type SessionHostRequestEnvelope, type SessionHostRequestTrace, type SessionHostRequestType, type SessionHostResponse, type SessionHostResponseEnvelope, type SessionHostRuntimeTransition, type SessionHostSurfaceKind, type SessionHostSurfaceRecordLike, type SessionHostWireEnvelope, type SessionLaunchCommand, type SessionLifecycle, type SessionOwnerType, SessionRingBuffer, type SessionRingBufferOptions, type SessionTerminalSnapshot, type SessionTerminalState, type SessionTermination, type SessionTransport, type SessionWriteOwner, type StopSessionPayload, type UpdateSessionMetaPayload, applyTerminalColorEnv, buildRuntimeDisplayName, buildRuntimeKey, canonicalizeInstancePath, classifyTermination, createLineParser, createResponseEnvelope, createSessionHostControlPlane, ensureNodePtySpawnHelperPermissions, formatRuntimeOwner, getDefaultSessionHostEndpoint, getSessionHostRecoveryLabel, getSessionHostSurfaceKind, getWorkspaceLabel, isDefaultInstanceConfigDir, isSessionHostLiveRuntime, isSessionHostRecoverySnapshot, isTerminalRecord, partitionSessionHostDiagnosticsSessions, partitionSessionHostRecords, resolveAttachableRuntimeRecord, resolveInstanceConfigDir, resolveRuntimeRecord, resolveSessionHostIpcKey, sanitizeSpawnEnv, writeEnvelope };
|
|
812
|
+
export { type AcquireWritePayload, type AttachSessionPayload, type ClearSessionBufferPayload, type CreateSessionPayload, DEFAULT_CONFIG_DIR_NAME, DEFAULT_SESSION_RING_BUFFER_MAX_BYTES, type DetachSessionPayload, type ForceDetachClientPayload, type GetHostDiagnosticsPayload, type GetSnapshotPayload, type GetTerminalSnapshotPayload, type PruneDuplicateSessionsPayload, type ReleaseWritePayload, type ResizeSessionPayload, type RestartSessionPayload, type ResumeSessionPayload, SESSION_HOST_SUPPORTED_REQUEST_TYPES, type SendInputPayload, type SendSignalPayload, type SessionAttachedClient, type SessionBufferSnapshot, type SessionClientType, type SessionHostCategory, SessionHostClient, type SessionHostClientOptions, type SessionHostControlPlane, type SessionHostControlTransport, type SessionHostDiagnostics, type SessionHostDisconnectInfo, type SessionHostDisconnectReason, type SessionHostDuplicateSessionGroup, type SessionHostEndpoint, type SessionHostEndpointOptions, type SessionHostEvent, type SessionHostEventEnvelope, type SessionHostLogEntry, type SessionHostPruneDuplicatesResult, type SessionHostRecord, SessionHostRegistry, type SessionHostRequest, type SessionHostRequestEnvelope, type SessionHostRequestTrace, type SessionHostRequestType, type SessionHostResponse, type SessionHostResponseEnvelope, type SessionHostRuntimeTransition, type SessionHostSnapshot, type SessionHostSurfaceKind, type SessionHostSurfaceRecordLike, type SessionHostWireEnvelope, type SessionLaunchCommand, type SessionLifecycle, type SessionOwnerType, SessionRingBuffer, type SessionRingBufferOptions, type SessionTerminalSnapshot, type SessionTerminalState, type SessionTermination, type SessionTransport, type SessionWriteOwner, type StopSessionPayload, type UpdateSessionMetaPayload, applyTerminalColorEnv, buildRuntimeDisplayName, buildRuntimeKey, canonicalizeInstancePath, classifyTermination, createLineParser, createResponseEnvelope, createSessionHostControlPlane, ensureNodePtySpawnHelperPermissions, formatRuntimeOwner, getDefaultSessionHostEndpoint, getSessionHostRecoveryLabel, getSessionHostSurfaceKind, getWorkspaceLabel, isDefaultInstanceConfigDir, isSessionHostLiveRuntime, isSessionHostRecoverySnapshot, isTerminalRecord, partitionSessionHostDiagnosticsSessions, partitionSessionHostRecords, resolveAttachableRuntimeRecord, resolveInstanceConfigDir, resolveRuntimeRecord, resolveSessionHostIpcKey, sanitizeSpawnEnv, writeEnvelope };
|
package/dist/index.d.ts
CHANGED
|
@@ -456,6 +456,47 @@ declare class SessionRingBuffer {
|
|
|
456
456
|
text: string;
|
|
457
457
|
}): void;
|
|
458
458
|
private trim;
|
|
459
|
+
/**
|
|
460
|
+
* TRIM-BOUNDARY repair. Eviction drops whole chunks, and a chunk boundary is
|
|
461
|
+
* a PTY read boundary — an arbitrary byte offset with no relationship to the
|
|
462
|
+
* structure of the stream. So the chunk that becomes the new oldest can begin
|
|
463
|
+
* partway through something the sender wrote atomically, and `snapshot()`
|
|
464
|
+
* joins from exactly there.
|
|
465
|
+
*
|
|
466
|
+
* The consumer is the browser terminal: "Load older terminal output" asks
|
|
467
|
+
* with `sinceSeq: 0`, which makes the daemon skip the emulator viewport and
|
|
468
|
+
* hand this raw text straight to xterm (see `mergeRuntimeSnapshot`). xterm
|
|
469
|
+
* then parses a stream that starts mid-token, which is how the reported
|
|
470
|
+
* screenshot got orphaned `.` and `5` glyphs floating above the output and a
|
|
471
|
+
* large blank band at the top:
|
|
472
|
+
*
|
|
473
|
+
* drop 5 chars -> "[HClaude Code v2.1.220…" the CSI introducer is gone,
|
|
474
|
+
* so `[H` prints literally
|
|
475
|
+
* drop 12 chars -> "2mClaude Code v2.1.220…" half an SGR prints literally
|
|
476
|
+
* drop 46 chars -> the leading `\x1b[2J\x1b[H` never arrives, so the screen
|
|
477
|
+
* is never cleared/homed and row placement collapses
|
|
478
|
+
* byte cut -> a torn 3-byte Hangul sequence decodes to U+FFFD
|
|
479
|
+
*
|
|
480
|
+
* So walk the head of the new oldest chunk forward to the first offset that
|
|
481
|
+
* is safe to start parsing at, and drop the partial prefix. Losing a few
|
|
482
|
+
* bytes of already-evicted context is strictly better than injecting literal
|
|
483
|
+
* garbage into the viewport.
|
|
484
|
+
*
|
|
485
|
+
* Two independent boundary classes have to be handled, and neither subsumes
|
|
486
|
+
* the other:
|
|
487
|
+
*
|
|
488
|
+
* 1. Character encoding. Chunks are JS strings, so a torn multi-byte UTF-8
|
|
489
|
+
* sequence has already decayed into U+FFFD (or, for astral characters, a
|
|
490
|
+
* lone surrogate) by the time it gets here. This mirrors the protection
|
|
491
|
+
* `createLineParser` grew for the IPC socket path in
|
|
492
|
+
* `ipc-line-parser-utf8.test.ts`; that layer can hold bytes back and
|
|
493
|
+
* re-join them because it owns both sides of the split, whereas here the
|
|
494
|
+
* other half is already gone, so dropping is the only repair available.
|
|
495
|
+
* 2. Escape sequences. A CSI/OSC/SS3 can be cut anywhere, and unlike the
|
|
496
|
+
* encoding case the leftover bytes are all perfectly printable — which is
|
|
497
|
+
* precisely why the corruption is visible rather than silent.
|
|
498
|
+
*/
|
|
499
|
+
private healHead;
|
|
459
500
|
}
|
|
460
501
|
|
|
461
502
|
declare class SessionHostRegistry {
|
|
@@ -708,8 +749,16 @@ declare function applyTerminalColorEnv(env: Record<string, string>): void;
|
|
|
708
749
|
*/
|
|
709
750
|
declare function ensureNodePtySpawnHelperPermissions(logFn?: (msg: string) => void): void;
|
|
710
751
|
|
|
752
|
+
/** Response shape of the `get_snapshot` wire request — a PTY snapshot for a session. */
|
|
753
|
+
interface SessionHostSnapshot {
|
|
754
|
+
seq: number;
|
|
755
|
+
text: string;
|
|
756
|
+
truncated: boolean;
|
|
757
|
+
cols?: number;
|
|
758
|
+
rows?: number;
|
|
759
|
+
}
|
|
711
760
|
/**
|
|
712
|
-
* The
|
|
761
|
+
* The session-host control-plane surface shared by the cloud and
|
|
713
762
|
* standalone daemons. Both daemons used to carry a byte-for-byte copy of this
|
|
714
763
|
* dispatch table plus the identical throw strings; this is the single source of
|
|
715
764
|
* truth for that mapping. The wire `type` strings and error text here are the
|
|
@@ -729,6 +778,14 @@ interface SessionHostControlPlane {
|
|
|
729
778
|
pruneDuplicateSessions(payload?: PruneDuplicateSessionsPayload): Promise<SessionHostPruneDuplicatesResult>;
|
|
730
779
|
acquireWrite(payload: AcquireWritePayload): Promise<SessionHostRecord | null>;
|
|
731
780
|
releaseWrite(payload: ReleaseWritePayload): Promise<SessionHostRecord | null>;
|
|
781
|
+
/**
|
|
782
|
+
* A PTY snapshot for `sessionId` (optionally only the tail since `sinceSeq`),
|
|
783
|
+
* over wire type `get_snapshot`. Mirrors the shape daemon-core's local
|
|
784
|
+
* `SessionHostControlPlane` interface (`commands/router.ts`) and cloud's
|
|
785
|
+
* former `cloud-command-transports.ts` P2P-only `get_runtime_snapshot` case
|
|
786
|
+
* both already used against a raw `SessionHostClient`.
|
|
787
|
+
*/
|
|
788
|
+
getSnapshot(sessionId: string, sinceSeq?: number): Promise<SessionHostSnapshot | null>;
|
|
732
789
|
}
|
|
733
790
|
/**
|
|
734
791
|
* Minimal request transport the control-plane factory needs. Each daemon injects
|
|
@@ -745,11 +802,11 @@ interface SessionHostControlTransport {
|
|
|
745
802
|
request<T>(type: SessionHostRequestType, payload: Record<string, unknown>): Promise<T>;
|
|
746
803
|
}
|
|
747
804
|
/**
|
|
748
|
-
* Build the shared
|
|
805
|
+
* Build the shared 12-method control-plane over an injected request transport.
|
|
749
806
|
* The (type string, payload shape) pairs are verbatim what both daemons emitted
|
|
750
807
|
* previously — do not reword them without matching the session-host daemon's
|
|
751
808
|
* request handlers.
|
|
752
809
|
*/
|
|
753
810
|
declare function createSessionHostControlPlane(transport: SessionHostControlTransport): SessionHostControlPlane;
|
|
754
811
|
|
|
755
|
-
export { type AcquireWritePayload, type AttachSessionPayload, type ClearSessionBufferPayload, type CreateSessionPayload, DEFAULT_CONFIG_DIR_NAME, DEFAULT_SESSION_RING_BUFFER_MAX_BYTES, type DetachSessionPayload, type ForceDetachClientPayload, type GetHostDiagnosticsPayload, type GetSnapshotPayload, type GetTerminalSnapshotPayload, type PruneDuplicateSessionsPayload, type ReleaseWritePayload, type ResizeSessionPayload, type RestartSessionPayload, type ResumeSessionPayload, SESSION_HOST_SUPPORTED_REQUEST_TYPES, type SendInputPayload, type SendSignalPayload, type SessionAttachedClient, type SessionBufferSnapshot, type SessionClientType, type SessionHostCategory, SessionHostClient, type SessionHostClientOptions, type SessionHostControlPlane, type SessionHostControlTransport, type SessionHostDiagnostics, type SessionHostDisconnectInfo, type SessionHostDisconnectReason, type SessionHostDuplicateSessionGroup, type SessionHostEndpoint, type SessionHostEndpointOptions, type SessionHostEvent, type SessionHostEventEnvelope, type SessionHostLogEntry, type SessionHostPruneDuplicatesResult, type SessionHostRecord, SessionHostRegistry, type SessionHostRequest, type SessionHostRequestEnvelope, type SessionHostRequestTrace, type SessionHostRequestType, type SessionHostResponse, type SessionHostResponseEnvelope, type SessionHostRuntimeTransition, type SessionHostSurfaceKind, type SessionHostSurfaceRecordLike, type SessionHostWireEnvelope, type SessionLaunchCommand, type SessionLifecycle, type SessionOwnerType, SessionRingBuffer, type SessionRingBufferOptions, type SessionTerminalSnapshot, type SessionTerminalState, type SessionTermination, type SessionTransport, type SessionWriteOwner, type StopSessionPayload, type UpdateSessionMetaPayload, applyTerminalColorEnv, buildRuntimeDisplayName, buildRuntimeKey, canonicalizeInstancePath, classifyTermination, createLineParser, createResponseEnvelope, createSessionHostControlPlane, ensureNodePtySpawnHelperPermissions, formatRuntimeOwner, getDefaultSessionHostEndpoint, getSessionHostRecoveryLabel, getSessionHostSurfaceKind, getWorkspaceLabel, isDefaultInstanceConfigDir, isSessionHostLiveRuntime, isSessionHostRecoverySnapshot, isTerminalRecord, partitionSessionHostDiagnosticsSessions, partitionSessionHostRecords, resolveAttachableRuntimeRecord, resolveInstanceConfigDir, resolveRuntimeRecord, resolveSessionHostIpcKey, sanitizeSpawnEnv, writeEnvelope };
|
|
812
|
+
export { type AcquireWritePayload, type AttachSessionPayload, type ClearSessionBufferPayload, type CreateSessionPayload, DEFAULT_CONFIG_DIR_NAME, DEFAULT_SESSION_RING_BUFFER_MAX_BYTES, type DetachSessionPayload, type ForceDetachClientPayload, type GetHostDiagnosticsPayload, type GetSnapshotPayload, type GetTerminalSnapshotPayload, type PruneDuplicateSessionsPayload, type ReleaseWritePayload, type ResizeSessionPayload, type RestartSessionPayload, type ResumeSessionPayload, SESSION_HOST_SUPPORTED_REQUEST_TYPES, type SendInputPayload, type SendSignalPayload, type SessionAttachedClient, type SessionBufferSnapshot, type SessionClientType, type SessionHostCategory, SessionHostClient, type SessionHostClientOptions, type SessionHostControlPlane, type SessionHostControlTransport, type SessionHostDiagnostics, type SessionHostDisconnectInfo, type SessionHostDisconnectReason, type SessionHostDuplicateSessionGroup, type SessionHostEndpoint, type SessionHostEndpointOptions, type SessionHostEvent, type SessionHostEventEnvelope, type SessionHostLogEntry, type SessionHostPruneDuplicatesResult, type SessionHostRecord, SessionHostRegistry, type SessionHostRequest, type SessionHostRequestEnvelope, type SessionHostRequestTrace, type SessionHostRequestType, type SessionHostResponse, type SessionHostResponseEnvelope, type SessionHostRuntimeTransition, type SessionHostSnapshot, type SessionHostSurfaceKind, type SessionHostSurfaceRecordLike, type SessionHostWireEnvelope, type SessionLaunchCommand, type SessionLifecycle, type SessionOwnerType, SessionRingBuffer, type SessionRingBufferOptions, type SessionTerminalSnapshot, type SessionTerminalState, type SessionTermination, type SessionTransport, type SessionWriteOwner, type StopSessionPayload, type UpdateSessionMetaPayload, applyTerminalColorEnv, buildRuntimeDisplayName, buildRuntimeKey, canonicalizeInstancePath, classifyTermination, createLineParser, createResponseEnvelope, createSessionHostControlPlane, ensureNodePtySpawnHelperPermissions, formatRuntimeOwner, getDefaultSessionHostEndpoint, getSessionHostRecoveryLabel, getSessionHostSurfaceKind, getWorkspaceLabel, isDefaultInstanceConfigDir, isSessionHostLiveRuntime, isSessionHostRecoverySnapshot, isTerminalRecord, partitionSessionHostDiagnosticsSessions, partitionSessionHostRecords, resolveAttachableRuntimeRecord, resolveInstanceConfigDir, resolveRuntimeRecord, resolveSessionHostIpcKey, sanitizeSpawnEnv, writeEnvelope };
|
package/dist/index.js
CHANGED
|
@@ -181,13 +181,124 @@ var SessionRingBuffer = class {
|
|
|
181
181
|
this.trim();
|
|
182
182
|
}
|
|
183
183
|
trim() {
|
|
184
|
+
let evicted = false;
|
|
184
185
|
while (this.totalBytes > this.maxBytes && this.chunks.length > 1) {
|
|
185
186
|
const removed = this.chunks.shift();
|
|
186
187
|
if (!removed) break;
|
|
187
188
|
this.totalBytes -= removed.bytes;
|
|
189
|
+
evicted = true;
|
|
188
190
|
}
|
|
191
|
+
if (evicted) this.healHead();
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* TRIM-BOUNDARY repair. Eviction drops whole chunks, and a chunk boundary is
|
|
195
|
+
* a PTY read boundary — an arbitrary byte offset with no relationship to the
|
|
196
|
+
* structure of the stream. So the chunk that becomes the new oldest can begin
|
|
197
|
+
* partway through something the sender wrote atomically, and `snapshot()`
|
|
198
|
+
* joins from exactly there.
|
|
199
|
+
*
|
|
200
|
+
* The consumer is the browser terminal: "Load older terminal output" asks
|
|
201
|
+
* with `sinceSeq: 0`, which makes the daemon skip the emulator viewport and
|
|
202
|
+
* hand this raw text straight to xterm (see `mergeRuntimeSnapshot`). xterm
|
|
203
|
+
* then parses a stream that starts mid-token, which is how the reported
|
|
204
|
+
* screenshot got orphaned `.` and `5` glyphs floating above the output and a
|
|
205
|
+
* large blank band at the top:
|
|
206
|
+
*
|
|
207
|
+
* drop 5 chars -> "[HClaude Code v2.1.220…" the CSI introducer is gone,
|
|
208
|
+
* so `[H` prints literally
|
|
209
|
+
* drop 12 chars -> "2mClaude Code v2.1.220…" half an SGR prints literally
|
|
210
|
+
* drop 46 chars -> the leading `\x1b[2J\x1b[H` never arrives, so the screen
|
|
211
|
+
* is never cleared/homed and row placement collapses
|
|
212
|
+
* byte cut -> a torn 3-byte Hangul sequence decodes to U+FFFD
|
|
213
|
+
*
|
|
214
|
+
* So walk the head of the new oldest chunk forward to the first offset that
|
|
215
|
+
* is safe to start parsing at, and drop the partial prefix. Losing a few
|
|
216
|
+
* bytes of already-evicted context is strictly better than injecting literal
|
|
217
|
+
* garbage into the viewport.
|
|
218
|
+
*
|
|
219
|
+
* Two independent boundary classes have to be handled, and neither subsumes
|
|
220
|
+
* the other:
|
|
221
|
+
*
|
|
222
|
+
* 1. Character encoding. Chunks are JS strings, so a torn multi-byte UTF-8
|
|
223
|
+
* sequence has already decayed into U+FFFD (or, for astral characters, a
|
|
224
|
+
* lone surrogate) by the time it gets here. This mirrors the protection
|
|
225
|
+
* `createLineParser` grew for the IPC socket path in
|
|
226
|
+
* `ipc-line-parser-utf8.test.ts`; that layer can hold bytes back and
|
|
227
|
+
* re-join them because it owns both sides of the split, whereas here the
|
|
228
|
+
* other half is already gone, so dropping is the only repair available.
|
|
229
|
+
* 2. Escape sequences. A CSI/OSC/SS3 can be cut anywhere, and unlike the
|
|
230
|
+
* encoding case the leftover bytes are all perfectly printable — which is
|
|
231
|
+
* precisely why the corruption is visible rather than silent.
|
|
232
|
+
*/
|
|
233
|
+
healHead() {
|
|
234
|
+
const head = this.chunks[0];
|
|
235
|
+
if (!head) return;
|
|
236
|
+
const repaired = stripDanglingPrefix(head.data);
|
|
237
|
+
if (repaired === head.data) return;
|
|
238
|
+
const bytes = Buffer.byteLength(repaired, "utf8");
|
|
239
|
+
this.totalBytes -= head.bytes - bytes;
|
|
240
|
+
head.data = repaired;
|
|
241
|
+
head.bytes = bytes;
|
|
189
242
|
}
|
|
190
243
|
};
|
|
244
|
+
var CSI_FINAL = /[\x40-\x7e]/;
|
|
245
|
+
var CSI_COMMON_FINAL = /[ABCDEFGHJKSTLMPX@mnchlsurdfgqit]/;
|
|
246
|
+
function stripDanglingPrefix(text) {
|
|
247
|
+
let i = 0;
|
|
248
|
+
while (i < text.length) {
|
|
249
|
+
const code = text.charCodeAt(i);
|
|
250
|
+
if (code === 65533) {
|
|
251
|
+
i += 1;
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
if (code >= 55296 && code <= 56319) {
|
|
255
|
+
const next = text.charCodeAt(i + 1);
|
|
256
|
+
if (next >= 56320 && next <= 57343) break;
|
|
257
|
+
i += 1;
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
if (code >= 56320 && code <= 57343) {
|
|
261
|
+
i += 1;
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
break;
|
|
265
|
+
}
|
|
266
|
+
const rest = text.slice(i);
|
|
267
|
+
const danglingLength = danglingEscapeTailLength(rest);
|
|
268
|
+
return danglingLength > 0 ? rest.slice(danglingLength) : rest;
|
|
269
|
+
}
|
|
270
|
+
function danglingEscapeTailLength(text) {
|
|
271
|
+
if (!text || text.charCodeAt(0) === 27) return 0;
|
|
272
|
+
let i = 0;
|
|
273
|
+
const hasIntroducer = text[0] === "[" || text[0] === "]";
|
|
274
|
+
if (hasIntroducer) i = 1;
|
|
275
|
+
const start = i;
|
|
276
|
+
while (i < text.length && /[0-9;?:<=>]/.test(text[i])) i += 1;
|
|
277
|
+
if (i >= text.length) return 0;
|
|
278
|
+
if (text[0] === "]") {
|
|
279
|
+
const bel = text.indexOf("\x07");
|
|
280
|
+
if (bel >= 0) return bel + 1;
|
|
281
|
+
return 0;
|
|
282
|
+
}
|
|
283
|
+
if (!CSI_FINAL.test(text[i])) return 0;
|
|
284
|
+
if (!hasIntroducer) {
|
|
285
|
+
if (i === start) return 0;
|
|
286
|
+
if (!CSI_COMMON_FINAL.test(text[i])) return 0;
|
|
287
|
+
}
|
|
288
|
+
return i + 1;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// src/uuid.ts
|
|
292
|
+
function generateUUID() {
|
|
293
|
+
if (typeof crypto !== "undefined" && crypto.randomUUID) {
|
|
294
|
+
return crypto.randomUUID();
|
|
295
|
+
}
|
|
296
|
+
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
|
|
297
|
+
const r = Math.random() * 16 | 0;
|
|
298
|
+
const v = c === "x" ? r : r & 3 | 8;
|
|
299
|
+
return v.toString(16);
|
|
300
|
+
});
|
|
301
|
+
}
|
|
191
302
|
|
|
192
303
|
// src/defaults.ts
|
|
193
304
|
var DEFAULT_SESSION_HOST_COLS = 80;
|
|
@@ -349,16 +460,6 @@ function formatRuntimeOwner(record) {
|
|
|
349
460
|
}
|
|
350
461
|
|
|
351
462
|
// src/registry.ts
|
|
352
|
-
function generateUUID() {
|
|
353
|
-
if (typeof crypto !== "undefined" && crypto.randomUUID) {
|
|
354
|
-
return crypto.randomUUID();
|
|
355
|
-
}
|
|
356
|
-
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
|
|
357
|
-
const r = Math.random() * 16 | 0;
|
|
358
|
-
const v = c === "x" ? r : r & 3 | 8;
|
|
359
|
-
return v.toString(16);
|
|
360
|
-
});
|
|
361
|
-
}
|
|
362
463
|
var SessionHostRegistry = class {
|
|
363
464
|
sessions = /* @__PURE__ */ new Map();
|
|
364
465
|
createSession(payload) {
|
|
@@ -571,26 +672,27 @@ var SessionHostRegistry = class {
|
|
|
571
672
|
writeOwner: record.writeOwner ? { ...record.writeOwner } : null,
|
|
572
673
|
attachedClients: record.attachedClients.map((client) => ({ ...client })),
|
|
573
674
|
buffer: { ...record.buffer },
|
|
574
|
-
|
|
675
|
+
// Deep: meta carries nested records (e.g. the daemon's `launchRecord`,
|
|
676
|
+
// wiring-unification Phase E), and a caller mutating a returned record
|
|
677
|
+
// must never reach into the registry's own copy.
|
|
678
|
+
meta: cloneMeta(record.meta)
|
|
575
679
|
};
|
|
576
680
|
}
|
|
577
681
|
};
|
|
682
|
+
function cloneMeta(meta) {
|
|
683
|
+
if (!meta) return {};
|
|
684
|
+
try {
|
|
685
|
+
return structuredClone(meta);
|
|
686
|
+
} catch {
|
|
687
|
+
return { ...meta };
|
|
688
|
+
}
|
|
689
|
+
}
|
|
578
690
|
|
|
579
691
|
// src/ipc.ts
|
|
580
692
|
var os = __toESM(require("os"));
|
|
581
693
|
var path2 = __toESM(require("path"));
|
|
582
694
|
var net = __toESM(require("net"));
|
|
583
695
|
var import_string_decoder = require("string_decoder");
|
|
584
|
-
function generateUUID2() {
|
|
585
|
-
if (typeof crypto !== "undefined" && crypto.randomUUID) {
|
|
586
|
-
return crypto.randomUUID();
|
|
587
|
-
}
|
|
588
|
-
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
|
|
589
|
-
const r = Math.random() * 16 | 0;
|
|
590
|
-
const v = c === "x" ? r : r & 3 | 8;
|
|
591
|
-
return v.toString(16);
|
|
592
|
-
});
|
|
593
|
-
}
|
|
594
696
|
function getDefaultSessionHostEndpoint(appName = "adhdev", options = {}) {
|
|
595
697
|
const rawKey = typeof options.ipcKey === "string" ? options.ipcKey.trim() : "";
|
|
596
698
|
if (rawKey && !/^[0-9a-f]{12}$/.test(rawKey)) {
|
|
@@ -766,7 +868,7 @@ var SessionHostClient = class {
|
|
|
766
868
|
async request(request) {
|
|
767
869
|
await this.connect();
|
|
768
870
|
if (!this.socket) throw new Error("Session host socket unavailable");
|
|
769
|
-
const requestId =
|
|
871
|
+
const requestId = generateUUID();
|
|
770
872
|
const envelope = {
|
|
771
873
|
kind: "request",
|
|
772
874
|
requestId,
|
|
@@ -960,6 +1062,9 @@ function createSessionHostControlPlane(transport) {
|
|
|
960
1062
|
},
|
|
961
1063
|
releaseWrite(payload) {
|
|
962
1064
|
return transport.request("release_write", payload);
|
|
1065
|
+
},
|
|
1066
|
+
getSnapshot(sessionId, sinceSeq) {
|
|
1067
|
+
return transport.request("get_snapshot", { sessionId, sinceSeq });
|
|
963
1068
|
}
|
|
964
1069
|
};
|
|
965
1070
|
}
|