@fitchmultz/pi-subagents 0.52.20 → 0.52.21
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/CHANGELOG.md +30 -2
- package/dist/agent/contracts.js +12 -3
- package/dist/agent/index.js +9 -9
- package/dist/agent/legacy.js +5 -5
- package/dist/agent/notifications.js +21 -14
- package/dist/agent/parent-check.js +38 -0
- package/dist/agent/process.js +45 -52
- package/dist/agent/runner.js +59 -64
- package/dist/agent/storage.js +16 -4
- package/dist/agent/view.js +1 -3
- package/dist/build-info.js +1 -1
- package/dist/pi-intercom/broker/broker.js +4 -26
- package/dist/pi-intercom/broker/client-park.d.ts +1 -1
- package/dist/pi-intercom/broker/client-park.js +22 -6
- package/dist/pi-intercom/broker/client.d.ts +3 -2
- package/dist/pi-intercom/broker/client.js +88 -62
- package/dist/pi-intercom/broker/connection.d.ts +3 -5
- package/dist/pi-intercom/broker/connection.js +48 -33
- package/dist/pi-intercom/broker/framing.d.ts +1 -1
- package/dist/pi-intercom/broker/framing.js +19 -12
- package/dist/pi-intercom/broker/replace-deliveries.js +38 -0
- package/dist/pi-intercom/connection.js +5 -8
- package/dist/pi-intercom/inbound-journal.js +5 -2
- package/dist/shared/journal-frames.js +19 -24
- package/docs/development.md +6 -0
- package/package.json +1 -1
- package/src/agent/contracts.ts +12 -3
- package/src/agent/index.ts +10 -10
- package/src/agent/legacy.ts +5 -5
- package/src/agent/notifications.ts +35 -15
- package/src/agent/parent-check.ts +40 -0
- package/src/agent/process.ts +53 -55
- package/src/agent/runner.ts +65 -65
- package/src/agent/storage.ts +18 -5
- package/src/agent/view.ts +1 -4
- package/src/pi-intercom/broker/broker.ts +5 -37
- package/src/pi-intercom/broker/client-park.ts +27 -6
- package/src/pi-intercom/broker/client.ts +91 -61
- package/src/pi-intercom/broker/connection.ts +58 -39
- package/src/pi-intercom/broker/framing.ts +24 -12
- package/src/pi-intercom/broker/replace-deliveries.ts +56 -0
- package/src/pi-intercom/connection.ts +9 -9
- package/src/pi-intercom/inbound-journal.ts +5 -2
- package/src/shared/journal-frames.ts +22 -23
|
@@ -43,16 +43,20 @@ interface ConnectionHandlers {
|
|
|
43
43
|
readonly closed: (error: Readonly<Error>, expected: boolean) => void;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
type ConnectionPhase =
|
|
47
|
+
| { readonly kind: "acquired" | "active" | "failed" | "closed" }
|
|
48
|
+
| {
|
|
49
|
+
readonly kind: "registering";
|
|
50
|
+
readonly resolve: () => void;
|
|
51
|
+
readonly reject: (error: Readonly<Error>) => void;
|
|
52
|
+
readonly timer: NodeJS.Timeout;
|
|
53
|
+
}
|
|
54
|
+
| { readonly kind: "closing"; readonly task: Promise<void>; readonly registered: boolean };
|
|
55
|
+
|
|
46
56
|
/** One socket owns registration, read/error listeners, and graceful shutdown. */
|
|
47
57
|
export class BrokerConnection {
|
|
48
|
-
private
|
|
49
|
-
private closing = false;
|
|
58
|
+
private phase: ConnectionPhase = { kind: "acquired" };
|
|
50
59
|
private failure: Readonly<Error> | null = null;
|
|
51
|
-
private registration: {
|
|
52
|
-
readonly resolve: () => void;
|
|
53
|
-
readonly reject: (error: Readonly<Error>) => void;
|
|
54
|
-
} | null = null;
|
|
55
|
-
private registrationTimer: NodeJS.Timeout | null = null;
|
|
56
60
|
private readonly reader: (data: Buffer) => void;
|
|
57
61
|
|
|
58
62
|
readonly socket: net.Socket;
|
|
@@ -69,12 +73,9 @@ export class BrokerConnection {
|
|
|
69
73
|
|
|
70
74
|
start(registration: unknown): Promise<void> {
|
|
71
75
|
return new Promise((resolve, reject) => {
|
|
72
|
-
this.
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
10000,
|
|
76
|
-
);
|
|
77
|
-
this.registrationTimer.unref();
|
|
76
|
+
const timer = setTimeout(() => this.failRegistration(new Error("Connection timeout")), 10000);
|
|
77
|
+
timer.unref();
|
|
78
|
+
this.phase = { kind: "registering", resolve, reject, timer };
|
|
78
79
|
try {
|
|
79
80
|
writeMessage(this.socket, registration);
|
|
80
81
|
} catch (error) {
|
|
@@ -84,23 +85,28 @@ export class BrokerConnection {
|
|
|
84
85
|
}
|
|
85
86
|
|
|
86
87
|
confirmRegistration(): void {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
88
|
+
const pending = this.takeRegistration();
|
|
89
|
+
if (pending !== undefined) {
|
|
90
|
+
this.phase = { kind: "active" };
|
|
91
|
+
pending.resolve();
|
|
92
|
+
}
|
|
91
93
|
}
|
|
92
94
|
|
|
93
|
-
private
|
|
94
|
-
|
|
95
|
-
|
|
95
|
+
private takeRegistration() {
|
|
96
|
+
const pending = this.phase;
|
|
97
|
+
if (pending.kind !== "registering") {
|
|
98
|
+
return;
|
|
96
99
|
}
|
|
97
|
-
|
|
98
|
-
this.
|
|
100
|
+
clearTimeout(pending.timer);
|
|
101
|
+
this.phase = { kind: "acquired" };
|
|
102
|
+
return pending;
|
|
99
103
|
}
|
|
100
104
|
|
|
101
105
|
private failRegistration(error: Readonly<Error>): void {
|
|
102
|
-
const pending = this.
|
|
103
|
-
this.
|
|
106
|
+
const pending = this.takeRegistration();
|
|
107
|
+
if (this.phase.kind !== "closing") {
|
|
108
|
+
this.phase = { kind: "failed" };
|
|
109
|
+
}
|
|
104
110
|
this.failure = error;
|
|
105
111
|
this.socket.destroy();
|
|
106
112
|
pending?.reject(error);
|
|
@@ -127,19 +133,23 @@ export class BrokerConnection {
|
|
|
127
133
|
};
|
|
128
134
|
|
|
129
135
|
private readonly onClose = (): void => {
|
|
130
|
-
const
|
|
131
|
-
this.
|
|
136
|
+
const closing = this.phase.kind === "closing";
|
|
137
|
+
const pending = this.takeRegistration();
|
|
138
|
+
this.phase = { kind: "closed" };
|
|
132
139
|
this.socket.off("data", this.reader);
|
|
133
140
|
this.socket.off("error", this.onError);
|
|
134
141
|
this.socket.off("close", this.onClose);
|
|
135
142
|
pending?.reject(new Error("Connection closed before registration"));
|
|
136
|
-
this.handlers.closed(this.failure ?? new Error("Client disconnected"),
|
|
143
|
+
this.handlers.closed(this.failure ?? new Error("Client disconnected"), closing);
|
|
137
144
|
};
|
|
138
145
|
|
|
146
|
+
private get registered(): boolean {
|
|
147
|
+
return this.phase.kind === "active" || (this.phase.kind === "closing" && this.phase.registered);
|
|
148
|
+
}
|
|
149
|
+
|
|
139
150
|
isActive(): boolean {
|
|
140
151
|
return (
|
|
141
|
-
this.
|
|
142
|
-
!this.closing &&
|
|
152
|
+
this.phase.kind === "active" &&
|
|
143
153
|
!this.socket.destroyed &&
|
|
144
154
|
!this.socket.writableEnded &&
|
|
145
155
|
this.socket.writable
|
|
@@ -147,15 +157,18 @@ export class BrokerConnection {
|
|
|
147
157
|
}
|
|
148
158
|
|
|
149
159
|
disconnect(): Promise<void> {
|
|
150
|
-
|
|
160
|
+
const phase = this.phase;
|
|
161
|
+
if (phase.kind === "closing") {
|
|
162
|
+
return phase.task;
|
|
163
|
+
}
|
|
164
|
+
if (phase.kind === "closed") {
|
|
165
|
+
return Promise.resolve();
|
|
166
|
+
}
|
|
167
|
+
const pending = this.takeRegistration();
|
|
168
|
+
pending?.reject(new Error("Client disconnected before registration"));
|
|
151
169
|
this.failure = null;
|
|
152
|
-
|
|
153
|
-
let settled = false;
|
|
170
|
+
const task = new Promise<void>((resolve) => {
|
|
154
171
|
const finish = () => {
|
|
155
|
-
if (settled) {
|
|
156
|
-
return;
|
|
157
|
-
}
|
|
158
|
-
settled = true;
|
|
159
172
|
clearTimeout(timeout);
|
|
160
173
|
this.socket.off("close", finish);
|
|
161
174
|
this.socket.off("error", onError);
|
|
@@ -169,12 +182,18 @@ export class BrokerConnection {
|
|
|
169
182
|
}, 2000);
|
|
170
183
|
this.socket.once("close", finish);
|
|
171
184
|
this.socket.once("error", onError);
|
|
172
|
-
|
|
185
|
+
});
|
|
186
|
+
this.phase = { kind: "closing", task, registered: phase.kind === "active" };
|
|
187
|
+
try {
|
|
188
|
+
if (phase.kind === "active" || phase.kind === "registering") {
|
|
173
189
|
writeMessage(this.socket, { type: "unregister" });
|
|
174
190
|
this.socket.end();
|
|
175
|
-
}
|
|
191
|
+
} else {
|
|
176
192
|
this.socket.destroy();
|
|
177
193
|
}
|
|
178
|
-
}
|
|
194
|
+
} catch {
|
|
195
|
+
this.socket.destroy();
|
|
196
|
+
}
|
|
197
|
+
return task;
|
|
179
198
|
}
|
|
180
199
|
}
|
|
@@ -90,10 +90,15 @@ class MessageBuffer {
|
|
|
90
90
|
}
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
+
type FrameState =
|
|
94
|
+
| { readonly kind: "header" }
|
|
95
|
+
| { readonly kind: "payload"; readonly length: number }
|
|
96
|
+
| { readonly kind: "failed" };
|
|
97
|
+
|
|
93
98
|
/**
|
|
94
99
|
* Create a message reader that handles partial reads.
|
|
95
100
|
* Calls onMessage for each complete message received.
|
|
96
|
-
* Protocol or handler errors are reported
|
|
101
|
+
* Protocol or handler errors end reading and are reported so the caller can close the socket.
|
|
97
102
|
*/
|
|
98
103
|
export function createMessageReader(
|
|
99
104
|
onMessage: (msg: unknown) => void,
|
|
@@ -101,32 +106,39 @@ export function createMessageReader(
|
|
|
101
106
|
limit = MAX_FRAME_SIZE_BYTES,
|
|
102
107
|
): (data: Buffer) => void {
|
|
103
108
|
const buffer = new MessageBuffer();
|
|
104
|
-
let
|
|
109
|
+
let state: FrameState = { kind: "header" };
|
|
110
|
+
const fail = (error: Readonly<Error>) => {
|
|
111
|
+
state = { kind: "failed" };
|
|
112
|
+
buffer.clear();
|
|
113
|
+
onError(error);
|
|
114
|
+
};
|
|
105
115
|
|
|
106
116
|
return (data: Buffer) => {
|
|
117
|
+
if (state.kind === "failed") {
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
107
120
|
buffer.append(data);
|
|
108
121
|
|
|
109
|
-
while (buffer.length >= (length
|
|
110
|
-
if (
|
|
111
|
-
length = buffer.read(4).readUInt32BE(0);
|
|
122
|
+
while (buffer.length >= (state.kind === "payload" ? state.length : 4)) {
|
|
123
|
+
if (state.kind === "header") {
|
|
124
|
+
const length = buffer.read(4).readUInt32BE(0);
|
|
112
125
|
if (length > limit) {
|
|
113
|
-
|
|
114
|
-
buffer.clear();
|
|
115
|
-
length = undefined;
|
|
126
|
+
fail(new Error(`Intercom frame too large (${length} bytes; max ${limit})`));
|
|
116
127
|
return;
|
|
117
128
|
}
|
|
129
|
+
state = { kind: "payload", length };
|
|
118
130
|
continue;
|
|
119
131
|
}
|
|
120
132
|
|
|
121
|
-
const payload = buffer.read(length);
|
|
122
|
-
|
|
133
|
+
const payload = buffer.read(state.length);
|
|
134
|
+
state = { kind: "header" };
|
|
123
135
|
|
|
124
136
|
let msg: unknown;
|
|
125
137
|
try {
|
|
126
138
|
msg = JSON.parse(payload.toString("utf-8"));
|
|
127
139
|
} catch (error) {
|
|
128
140
|
const message = errorMessage(error);
|
|
129
|
-
|
|
141
|
+
fail(new Error(`Failed to parse intercom message: ${message}`, { cause: error }));
|
|
130
142
|
return;
|
|
131
143
|
}
|
|
132
144
|
|
|
@@ -134,7 +146,7 @@ export function createMessageReader(
|
|
|
134
146
|
onMessage(msg);
|
|
135
147
|
} catch (error) {
|
|
136
148
|
const message = errorMessage(error);
|
|
137
|
-
|
|
149
|
+
fail(new Error(`Failed to handle intercom message: ${message}`, { cause: error }));
|
|
138
150
|
return;
|
|
139
151
|
}
|
|
140
152
|
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { Message, SessionInfo } from "../types.ts";
|
|
2
|
+
|
|
3
|
+
const REPLACE_DELIVERY_DELAY_MS = 1500;
|
|
4
|
+
|
|
5
|
+
interface StagedDelivery {
|
|
6
|
+
readonly fromId: string;
|
|
7
|
+
readonly toId: string;
|
|
8
|
+
readonly from: SessionInfo;
|
|
9
|
+
readonly message: Message;
|
|
10
|
+
readonly timer: NodeJS.Timeout;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Only broker-staged messages remain replaceable; forwarding ends this owner's authority. */
|
|
14
|
+
export class ReplaceDeliveries {
|
|
15
|
+
private readonly pending = new Map<string, StagedDelivery>();
|
|
16
|
+
private readonly deliver: (toId: string, from: SessionInfo, message: Message) => string | null;
|
|
17
|
+
|
|
18
|
+
constructor(deliver: (toId: string, from: SessionInfo, message: Message) => string | null) {
|
|
19
|
+
this.deliver = deliver;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
stage(fromId: string, toId: string, from: SessionInfo, message: Message): void {
|
|
23
|
+
const key = `${fromId}\0${toId}\0${message.threadId ?? ""}`;
|
|
24
|
+
this.take(key);
|
|
25
|
+
const timer = setTimeout(() => this.flush(key), REPLACE_DELIVERY_DELAY_MS);
|
|
26
|
+
timer.unref();
|
|
27
|
+
this.pending.set(key, { fromId, toId, from, message, timer });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
private take(key: string): StagedDelivery | undefined {
|
|
31
|
+
const pending = this.pending.get(key);
|
|
32
|
+
if (pending !== undefined) {
|
|
33
|
+
this.pending.delete(key);
|
|
34
|
+
clearTimeout(pending.timer);
|
|
35
|
+
}
|
|
36
|
+
return pending;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
private flush(key: string): void {
|
|
40
|
+
const pending = this.take(key);
|
|
41
|
+
if (pending !== undefined) {
|
|
42
|
+
this.deliver(pending.toId, pending.from, pending.message);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
disconnected(sessionId: string): void {
|
|
47
|
+
for (const [key, pending] of this.pending) {
|
|
48
|
+
if (
|
|
49
|
+
pending.toId === sessionId ||
|
|
50
|
+
(pending.fromId === sessionId && pending.message.expectsReply === true)
|
|
51
|
+
) {
|
|
52
|
+
this.take(key);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -43,8 +43,10 @@ export class IntercomConnection {
|
|
|
43
43
|
private parked: IntercomClient | null = null;
|
|
44
44
|
private reconnectTimer: NodeJS.Timeout | null = null;
|
|
45
45
|
private startupTimer: NodeJS.Timeout | null = null;
|
|
46
|
-
private connecting:
|
|
47
|
-
|
|
46
|
+
private connecting: {
|
|
47
|
+
readonly task: Promise<IntercomTransport>;
|
|
48
|
+
readonly generation: number;
|
|
49
|
+
} | null = null;
|
|
48
50
|
private attempt = 0;
|
|
49
51
|
private readonly lifecycle: Lifecycle;
|
|
50
52
|
private readonly config: IntercomConfig;
|
|
@@ -352,8 +354,8 @@ export class IntercomConnection {
|
|
|
352
354
|
if (!ctx || generation !== this.lifecycle.generation) {
|
|
353
355
|
throw new Error("Intercom shutting down");
|
|
354
356
|
}
|
|
355
|
-
if (this.connecting
|
|
356
|
-
return this.connecting;
|
|
357
|
+
if (this.connecting?.generation === generation) {
|
|
358
|
+
return this.connecting.task;
|
|
357
359
|
}
|
|
358
360
|
if (this.client?.isConnected() === true) {
|
|
359
361
|
return this.client;
|
|
@@ -365,8 +367,7 @@ export class IntercomConnection {
|
|
|
365
367
|
this.clearReconnect();
|
|
366
368
|
// Publish ownership before registration, which can synchronously invoke subscribers.
|
|
367
369
|
const attempt = Promise.resolve().then(() => this.connect(ctx, generation, sessionId));
|
|
368
|
-
this.connecting = attempt;
|
|
369
|
-
this.connectingGeneration = generation;
|
|
370
|
+
this.connecting = { task: attempt, generation };
|
|
370
371
|
return await this.finishAttempt(attempt, ctx, generation);
|
|
371
372
|
}
|
|
372
373
|
private async finishAttempt(
|
|
@@ -381,9 +382,8 @@ export class IntercomConnection {
|
|
|
381
382
|
retry = this.lifecycle.live(ctx, generation) !== null;
|
|
382
383
|
throw error;
|
|
383
384
|
} finally {
|
|
384
|
-
if (this.connecting === attempt) {
|
|
385
|
+
if (this.connecting?.task === attempt) {
|
|
385
386
|
this.connecting = null;
|
|
386
|
-
this.connectingGeneration = null;
|
|
387
387
|
}
|
|
388
388
|
if (retry) {
|
|
389
389
|
this.scheduleReconnect();
|
|
@@ -394,7 +394,7 @@ export class IntercomConnection {
|
|
|
394
394
|
this.clearTimers();
|
|
395
395
|
this.cancelWakes();
|
|
396
396
|
// The bounded launcher attempt is joined before socket shutdown.
|
|
397
|
-
await Promise.allSettled([this.connecting]);
|
|
397
|
+
await Promise.allSettled([this.connecting?.task]);
|
|
398
398
|
if (this.client) {
|
|
399
399
|
await this.client.disconnect();
|
|
400
400
|
this.client = null;
|
|
@@ -191,13 +191,16 @@ export class InboundJournal {
|
|
|
191
191
|
pending.from.id === entry.from.id &&
|
|
192
192
|
pending.message.threadId === entry.message.threadId
|
|
193
193
|
) {
|
|
194
|
-
this.
|
|
195
|
-
this.replies.markReplied(pending.message.id);
|
|
194
|
+
this.supersede(pending.message.id);
|
|
196
195
|
}
|
|
197
196
|
}
|
|
198
197
|
}
|
|
199
198
|
this.pending.set(entry.message.id, entry);
|
|
200
199
|
}
|
|
200
|
+
private supersede(id: string): void {
|
|
201
|
+
this.consume(id);
|
|
202
|
+
this.replies.markReplied(id);
|
|
203
|
+
}
|
|
201
204
|
setStage(entry: PendingInboundMessage, stage: PendingInboundMessage["stage"]): void {
|
|
202
205
|
if (entry.stage === stage) {
|
|
203
206
|
return;
|
|
@@ -16,13 +16,15 @@ export interface JournalOptions {
|
|
|
16
16
|
readonly stringChunk?: StringChunk | undefined;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
type RecordState =
|
|
20
|
+
| { readonly kind: "blank" | "reading" }
|
|
21
|
+
| { readonly kind: "invalid"; readonly error: unknown };
|
|
22
|
+
|
|
19
23
|
/** Byte-framed JSONL. Publication framing is independent of malformed-record tolerance. */
|
|
20
24
|
export class JournalFrames {
|
|
21
25
|
private projection: JsonProjection;
|
|
22
26
|
private decoder = new TextDecoder("utf8", { fatal: true });
|
|
23
|
-
private
|
|
24
|
-
private failed = false;
|
|
25
|
-
private nonblank = false;
|
|
27
|
+
private state: RecordState = { kind: "blank" };
|
|
26
28
|
private start: number;
|
|
27
29
|
offset: number;
|
|
28
30
|
private readonly policy: JournalPolicy;
|
|
@@ -48,20 +50,20 @@ export class JournalFrames {
|
|
|
48
50
|
return new JsonProjection(this.select, this.options.stringChunk);
|
|
49
51
|
}
|
|
50
52
|
private part(bytes: Buffer): void {
|
|
51
|
-
if (
|
|
52
|
-
|
|
53
|
+
if (this.state.kind === "invalid") {
|
|
54
|
+
return;
|
|
53
55
|
}
|
|
54
|
-
if (
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
}
|
|
56
|
+
if (this.state.kind === "blank" && bytes.some((byte) => ![9, 10, 13, 32].includes(byte))) {
|
|
57
|
+
this.state = { kind: "reading" };
|
|
58
|
+
}
|
|
59
|
+
try {
|
|
60
|
+
this.projection.write(this.decoder.decode(bytes, { stream: true }));
|
|
61
|
+
} catch (error) {
|
|
62
|
+
this.state = { kind: "invalid", error };
|
|
61
63
|
}
|
|
62
64
|
}
|
|
63
65
|
private finishRecord(end: number): JournalRecord | undefined {
|
|
64
|
-
if (
|
|
66
|
+
if (this.state.kind !== "reading") {
|
|
65
67
|
return undefined;
|
|
66
68
|
}
|
|
67
69
|
try {
|
|
@@ -72,17 +74,16 @@ export class JournalFrames {
|
|
|
72
74
|
}
|
|
73
75
|
return { value, start: this.start, end };
|
|
74
76
|
} catch (error) {
|
|
75
|
-
this.
|
|
76
|
-
this.error = error;
|
|
77
|
+
this.state = { kind: "invalid", error };
|
|
77
78
|
return undefined;
|
|
78
79
|
}
|
|
79
80
|
}
|
|
80
81
|
private commit(end: number): void {
|
|
81
82
|
const start = this.start;
|
|
82
83
|
const record = this.finishRecord(end);
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
|
|
84
|
+
const state = this.state;
|
|
85
|
+
if (state.kind === "invalid" && this.policy !== "inspect") {
|
|
86
|
+
const { error } = state;
|
|
86
87
|
throw new SyntaxError(`Invalid JSONL record at byte ${start}: ${errorDescription(error)}`, {
|
|
87
88
|
cause: error,
|
|
88
89
|
});
|
|
@@ -90,10 +91,8 @@ export class JournalFrames {
|
|
|
90
91
|
this.start = end;
|
|
91
92
|
this.projection = this.newProjection();
|
|
92
93
|
this.decoder = new TextDecoder("utf8", { fatal: true });
|
|
93
|
-
this.
|
|
94
|
-
|
|
95
|
-
this.nonblank = false;
|
|
96
|
-
if (!failed && record) {
|
|
94
|
+
this.state = { kind: "blank" };
|
|
95
|
+
if (record) {
|
|
97
96
|
this.record(record);
|
|
98
97
|
}
|
|
99
98
|
}
|
|
@@ -110,7 +109,7 @@ export class JournalFrames {
|
|
|
110
109
|
}
|
|
111
110
|
finish(): number {
|
|
112
111
|
if (this.requireNewline) {
|
|
113
|
-
if (this.policy === "strict" && this.
|
|
112
|
+
if (this.policy === "strict" && this.state.kind !== "blank") {
|
|
114
113
|
throw new SyntaxError(`Unpublished JSONL record at byte ${this.start}: newline required`);
|
|
115
114
|
}
|
|
116
115
|
} else if (this.offset > this.start) {
|