hilos-agent 0.10.1 → 0.11.2
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/LICENSE +21 -0
- package/README.md +54 -10
- package/bin/hilos-agent.mjs +87 -14
- package/package.json +7 -5
- package/src/acp-session.mjs +16 -1
- package/src/cli.mjs +34 -1
- package/src/codex-mcp-session.mjs +36 -0
- package/src/config.mjs +39 -5
- package/src/conflicts.mjs +156 -0
- package/src/cursor-store.mjs +43 -8
- package/src/handler.mjs +1197 -127
- package/src/iterate-claim-recovery.mjs +574 -0
- package/src/local-harness-events.mjs +370 -0
- package/src/local-harness-transport.mjs +157 -0
- package/src/mcp.mjs +411 -34
- package/src/model-resolve.mjs +3 -1
- package/src/opencode-session.mjs +22 -2
- package/src/progress-emitter.mjs +1 -1
- package/src/queue.mjs +70 -3
- package/src/redact.mjs +360 -7
- package/src/run-inputs.mjs +211 -0
- package/src/run.mjs +462 -76
- package/src/thread-pr.mjs +26 -4
- package/src/webmcp-bridge.mjs +77 -4
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
// Local coding-runtime stream -> Harness Event Protocol adapter.
|
|
2
|
+
//
|
|
3
|
+
// This is deliberately a side projection of agent-events.mjs. The current
|
|
4
|
+
// progress card keeps consuming its existing AgentEvent stream unchanged; this
|
|
5
|
+
// module reuses that parser to produce replayable HEP inputs without performing
|
|
6
|
+
// a network, storage, or transcript write. A caller must provide a streamId
|
|
7
|
+
// which is stable for the lifetime of one runtime process. Replaying the same
|
|
8
|
+
// bytes with that id produces the same idempotency keys, independent of chunk
|
|
9
|
+
// boundaries.
|
|
10
|
+
|
|
11
|
+
import { createHash } from "node:crypto";
|
|
12
|
+
|
|
13
|
+
import { makeStreamParser } from "./agent-events.mjs";
|
|
14
|
+
|
|
15
|
+
export const LOCAL_HARNESS_LINE_MAX_CHARS = 256 * 1024;
|
|
16
|
+
export const LOCAL_HARNESS_EVENTS_PER_LINE_MAX = 64;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @typedef {object} LocalHarnessContext
|
|
20
|
+
* @property {string} runtime Claude Code, Codex, Cursor, or OpenCode vendor id.
|
|
21
|
+
* @property {string} streamId Stable id for one spawned runtime process. It is
|
|
22
|
+
* hashed before it enters an event.
|
|
23
|
+
* @property {string} actorId Hilos agent UUID which owns the local run.
|
|
24
|
+
* @property {string} [lane]
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The dependency-free package cannot import the app's TypeScript type, so this
|
|
29
|
+
* JSDoc shape intentionally mirrors `HarnessEventInput` in lib/harness-events.
|
|
30
|
+
* @typedef {object} LocalHarnessEventInput
|
|
31
|
+
* @property {string} type
|
|
32
|
+
* @property {number} version
|
|
33
|
+
* @property {string} lane
|
|
34
|
+
* @property {string} runtime
|
|
35
|
+
* @property {string} turnId
|
|
36
|
+
* @property {{type: 'agent', id: string}} actor
|
|
37
|
+
* @property {string} [summary]
|
|
38
|
+
* @property {'prose'|'command'} [summaryKind]
|
|
39
|
+
* @property {Record<string, unknown>} payload
|
|
40
|
+
* @property {string} idempotencyKey
|
|
41
|
+
*/
|
|
42
|
+
|
|
43
|
+
/** Canonical HEP runtime slug for the aliases accepted by makeStreamParser. */
|
|
44
|
+
export function normalizeLocalHarnessRuntime(runtime) {
|
|
45
|
+
const value = String(runtime || "").toLowerCase();
|
|
46
|
+
if (value === "claude" || value === "claude-code" || value === "claude_code") {
|
|
47
|
+
return "claude_code";
|
|
48
|
+
}
|
|
49
|
+
if (value === "codex" || value === "cursor" || value === "opencode") return value;
|
|
50
|
+
return "custom";
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function runtimeLabel(runtime) {
|
|
54
|
+
if (runtime === "claude_code") return "Claude Code";
|
|
55
|
+
if (runtime === "codex") return "Codex";
|
|
56
|
+
if (runtime === "cursor") return "Cursor";
|
|
57
|
+
if (runtime === "opencode") return "OpenCode";
|
|
58
|
+
return "The coding runtime";
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function digest(value) {
|
|
62
|
+
return createHash("sha256").update(value).digest("hex");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function streamDigest(runtime, streamId) {
|
|
66
|
+
return digest(`local-hep-v1\0${runtime}\0${String(streamId ?? "")}`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function eventKey(streamHash, sourcePosition, ordinal, type) {
|
|
70
|
+
return `local-stream-v1:${digest(
|
|
71
|
+
`${streamHash}\0${sourcePosition}\0${ordinal}\0${type}`,
|
|
72
|
+
)}`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function eventSpec(event, runtime) {
|
|
76
|
+
const label = runtimeLabel(runtime);
|
|
77
|
+
switch (event && event.t) {
|
|
78
|
+
case "session":
|
|
79
|
+
return {
|
|
80
|
+
type: "run.phase.changed",
|
|
81
|
+
summary: `${label} session started.`,
|
|
82
|
+
summaryKind: "prose",
|
|
83
|
+
payload: {
|
|
84
|
+
normalizedKind: "session",
|
|
85
|
+
phase: "runtime_ready",
|
|
86
|
+
...(event.sessionId ? { sessionId: event.sessionId } : {}),
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
case "phase":
|
|
90
|
+
return {
|
|
91
|
+
type: "run.phase.changed",
|
|
92
|
+
...(event.name
|
|
93
|
+
? { summary: `${label} entered ${event.name}.`, summaryKind: "prose" }
|
|
94
|
+
: {}),
|
|
95
|
+
payload: {
|
|
96
|
+
normalizedKind: "phase",
|
|
97
|
+
...(event.name ? { phase: event.name } : {}),
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
case "edit":
|
|
101
|
+
return {
|
|
102
|
+
type: "tool.started",
|
|
103
|
+
summary: event.path ? `Editing ${event.path}` : "Editing files",
|
|
104
|
+
summaryKind: "prose",
|
|
105
|
+
payload: {
|
|
106
|
+
normalizedKind: "edit",
|
|
107
|
+
tool: "file_edit",
|
|
108
|
+
...(event.path ? { path: event.path } : {}),
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
case "read":
|
|
112
|
+
return {
|
|
113
|
+
type: "tool.started",
|
|
114
|
+
summary: event.path ? `Reading ${event.path}` : "Reading files",
|
|
115
|
+
summaryKind: "prose",
|
|
116
|
+
payload: {
|
|
117
|
+
normalizedKind: "read",
|
|
118
|
+
tool: "file_read",
|
|
119
|
+
...(event.path ? { path: event.path } : {}),
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
case "run":
|
|
123
|
+
return {
|
|
124
|
+
type: "tool.started",
|
|
125
|
+
...(event.cmd ? { summary: event.cmd, summaryKind: "command" } : {}),
|
|
126
|
+
payload: {
|
|
127
|
+
normalizedKind: "run",
|
|
128
|
+
tool: "shell",
|
|
129
|
+
...(event.cmd ? { command: event.cmd } : {}),
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
case "websearch":
|
|
133
|
+
return {
|
|
134
|
+
type: "tool.started",
|
|
135
|
+
summary: event.query ? `Searching the web for ${event.query}` : "Searching the web",
|
|
136
|
+
summaryKind: "prose",
|
|
137
|
+
payload: {
|
|
138
|
+
normalizedKind: "websearch",
|
|
139
|
+
tool: "web_search",
|
|
140
|
+
...(event.query ? { query: event.query } : {}),
|
|
141
|
+
},
|
|
142
|
+
};
|
|
143
|
+
case "webfetch":
|
|
144
|
+
return {
|
|
145
|
+
type: "tool.started",
|
|
146
|
+
summary: event.target ? `Reading ${event.target}` : "Reading a web page",
|
|
147
|
+
summaryKind: "prose",
|
|
148
|
+
payload: {
|
|
149
|
+
normalizedKind: "webfetch",
|
|
150
|
+
tool: "web_fetch",
|
|
151
|
+
...(event.target ? { target: event.target } : {}),
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
case "subagent":
|
|
155
|
+
return {
|
|
156
|
+
type: "child.started",
|
|
157
|
+
...(event.text
|
|
158
|
+
? { summary: `Started a helper: ${event.text}`, summaryKind: "prose" }
|
|
159
|
+
: {}),
|
|
160
|
+
payload: {
|
|
161
|
+
normalizedKind: "subagent",
|
|
162
|
+
...(event.text ? { description: event.text } : {}),
|
|
163
|
+
},
|
|
164
|
+
};
|
|
165
|
+
case "note":
|
|
166
|
+
if (!event.text) return null;
|
|
167
|
+
return {
|
|
168
|
+
type: "note.recorded",
|
|
169
|
+
summary: event.text,
|
|
170
|
+
summaryKind: "prose",
|
|
171
|
+
payload: { normalizedKind: "note", text: event.text },
|
|
172
|
+
};
|
|
173
|
+
case "usage":
|
|
174
|
+
return {
|
|
175
|
+
type: "usage.updated",
|
|
176
|
+
summary: `${label} reported token usage.`,
|
|
177
|
+
summaryKind: "prose",
|
|
178
|
+
payload: {
|
|
179
|
+
normalizedKind: "usage",
|
|
180
|
+
inputTokens: event.inputTokens ?? 0,
|
|
181
|
+
outputTokens: event.outputTokens ?? 0,
|
|
182
|
+
cacheReadTokens: event.cacheReadTokens ?? 0,
|
|
183
|
+
cacheCreationTokens: event.cacheCreationTokens ?? 0,
|
|
184
|
+
...(event.costUsd == null ? {} : { costUsd: event.costUsd }),
|
|
185
|
+
...(event.model ? { model: event.model } : {}),
|
|
186
|
+
},
|
|
187
|
+
};
|
|
188
|
+
case "result":
|
|
189
|
+
return {
|
|
190
|
+
type: "turn.completed",
|
|
191
|
+
...(event.summary
|
|
192
|
+
? { summary: event.summary, summaryKind: "prose" }
|
|
193
|
+
: {}),
|
|
194
|
+
payload: {
|
|
195
|
+
normalizedKind: "result",
|
|
196
|
+
ok: event.ok !== false,
|
|
197
|
+
outcome: event.ok === false ? "failed" : "succeeded",
|
|
198
|
+
},
|
|
199
|
+
};
|
|
200
|
+
default:
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Stateful raw-stream adapter. `push` accepts arbitrary stdout chunks and
|
|
207
|
+
* `flush` must be called exactly once at process EOF. Neither method throws for
|
|
208
|
+
* malformed runtime output.
|
|
209
|
+
*
|
|
210
|
+
* @param {LocalHarnessContext} context
|
|
211
|
+
*/
|
|
212
|
+
export function createLocalHarnessStreamAdapter(context) {
|
|
213
|
+
const runtime = normalizeLocalHarnessRuntime(context?.runtime);
|
|
214
|
+
const parser = makeStreamParser(runtime);
|
|
215
|
+
const streamHash = streamDigest(runtime, context?.streamId);
|
|
216
|
+
const turnId = `local_${streamHash.slice(0, 32)}`;
|
|
217
|
+
const lane = context?.lane || "local";
|
|
218
|
+
const actorId = String(context?.actorId || "");
|
|
219
|
+
let buffer = "";
|
|
220
|
+
let lineNumber = 0;
|
|
221
|
+
let discardedChars = 0;
|
|
222
|
+
let discarding = false;
|
|
223
|
+
let structuredEventNumber = 0;
|
|
224
|
+
let finished = false;
|
|
225
|
+
|
|
226
|
+
const materialize = (events, sourcePosition, sourceLine, sourcePhase = "line") => {
|
|
227
|
+
const specs = events.map((event) => eventSpec(event, runtime)).filter(Boolean);
|
|
228
|
+
let bounded = specs;
|
|
229
|
+
if (specs.length > LOCAL_HARNESS_EVENTS_PER_LINE_MAX) {
|
|
230
|
+
bounded = [
|
|
231
|
+
...specs.slice(0, LOCAL_HARNESS_EVENTS_PER_LINE_MAX - 1),
|
|
232
|
+
{
|
|
233
|
+
type: "note.recorded",
|
|
234
|
+
summary: "Additional runtime events were withheld.",
|
|
235
|
+
summaryKind: "prose",
|
|
236
|
+
payload: {
|
|
237
|
+
withheld: true,
|
|
238
|
+
reason: "runtime_line_event_limit",
|
|
239
|
+
omittedEvents: specs.length - (LOCAL_HARNESS_EVENTS_PER_LINE_MAX - 1),
|
|
240
|
+
},
|
|
241
|
+
},
|
|
242
|
+
];
|
|
243
|
+
}
|
|
244
|
+
return bounded.map((spec, ordinal) => ({
|
|
245
|
+
type: spec.type,
|
|
246
|
+
version: 1,
|
|
247
|
+
lane,
|
|
248
|
+
runtime,
|
|
249
|
+
turnId,
|
|
250
|
+
actor: { type: "agent", id: actorId },
|
|
251
|
+
...(spec.summary === undefined ? {} : { summary: spec.summary }),
|
|
252
|
+
...(spec.summaryKind === undefined ? {} : { summaryKind: spec.summaryKind }),
|
|
253
|
+
payload: {
|
|
254
|
+
source: "runtime_stream",
|
|
255
|
+
sourceLine,
|
|
256
|
+
sourcePhase,
|
|
257
|
+
...spec.payload,
|
|
258
|
+
},
|
|
259
|
+
idempotencyKey: eventKey(streamHash, sourcePosition, ordinal, spec.type),
|
|
260
|
+
}));
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
const tooLarge = (sourceLine, originalCharacters) =>
|
|
264
|
+
materialize(
|
|
265
|
+
[
|
|
266
|
+
{
|
|
267
|
+
t: "note",
|
|
268
|
+
text: "A runtime event was too large to inspect.",
|
|
269
|
+
},
|
|
270
|
+
],
|
|
271
|
+
`line:${sourceLine}:withheld`,
|
|
272
|
+
sourceLine,
|
|
273
|
+
).map((event) => ({
|
|
274
|
+
...event,
|
|
275
|
+
payload: {
|
|
276
|
+
...event.payload,
|
|
277
|
+
withheld: true,
|
|
278
|
+
reason: "runtime_line_too_large",
|
|
279
|
+
originalCharacters,
|
|
280
|
+
},
|
|
281
|
+
}));
|
|
282
|
+
|
|
283
|
+
const consumeLine = (line) => {
|
|
284
|
+
lineNumber += 1;
|
|
285
|
+
if (line.length > LOCAL_HARNESS_LINE_MAX_CHARS) {
|
|
286
|
+
return tooLarge(lineNumber, line.length);
|
|
287
|
+
}
|
|
288
|
+
return materialize(parser.push(`${line}\n`), `line:${lineNumber}`, lineNumber);
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
return {
|
|
292
|
+
/** @param {string} chunk @returns {LocalHarnessEventInput[]} */
|
|
293
|
+
push(chunk) {
|
|
294
|
+
if (finished) return [];
|
|
295
|
+
const input = String(chunk ?? "");
|
|
296
|
+
const out = [];
|
|
297
|
+
let start = 0;
|
|
298
|
+
while (start < input.length) {
|
|
299
|
+
const newline = input.indexOf("\n", start);
|
|
300
|
+
const end = newline === -1 ? input.length : newline;
|
|
301
|
+
const fragment = input.slice(start, end);
|
|
302
|
+
if (discarding) {
|
|
303
|
+
discardedChars += fragment.length;
|
|
304
|
+
} else {
|
|
305
|
+
buffer += fragment;
|
|
306
|
+
if (buffer.length > LOCAL_HARNESS_LINE_MAX_CHARS) {
|
|
307
|
+
discarding = true;
|
|
308
|
+
discardedChars = buffer.length;
|
|
309
|
+
buffer = "";
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
if (newline === -1) break;
|
|
313
|
+
if (discarding) {
|
|
314
|
+
lineNumber += 1;
|
|
315
|
+
out.push(...tooLarge(lineNumber, discardedChars));
|
|
316
|
+
discarding = false;
|
|
317
|
+
discardedChars = 0;
|
|
318
|
+
} else {
|
|
319
|
+
out.push(...consumeLine(buffer));
|
|
320
|
+
buffer = "";
|
|
321
|
+
}
|
|
322
|
+
start = newline + 1;
|
|
323
|
+
}
|
|
324
|
+
return out;
|
|
325
|
+
},
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Structured permission transports deliver normalized AgentEvents beside
|
|
329
|
+
* stdout rather than encoding tool calls into it. Keep those facts in the
|
|
330
|
+
* same stream with their own deterministic process-local coordinate.
|
|
331
|
+
* @param {Record<string, unknown>} event
|
|
332
|
+
* @returns {LocalHarnessEventInput[]}
|
|
333
|
+
*/
|
|
334
|
+
pushEvent(event) {
|
|
335
|
+
if (finished) return [];
|
|
336
|
+
structuredEventNumber += 1;
|
|
337
|
+
return materialize(
|
|
338
|
+
[event],
|
|
339
|
+
`event:${structuredEventNumber}`,
|
|
340
|
+
lineNumber || 1,
|
|
341
|
+
"event",
|
|
342
|
+
);
|
|
343
|
+
},
|
|
344
|
+
|
|
345
|
+
/** @returns {LocalHarnessEventInput[]} */
|
|
346
|
+
flush() {
|
|
347
|
+
if (finished) return [];
|
|
348
|
+
finished = true;
|
|
349
|
+
const out = [];
|
|
350
|
+
if (discarding) {
|
|
351
|
+
lineNumber += 1;
|
|
352
|
+
out.push(...tooLarge(lineNumber, discardedChars));
|
|
353
|
+
} else if (buffer.length) {
|
|
354
|
+
out.push(...consumeLine(buffer));
|
|
355
|
+
}
|
|
356
|
+
buffer = "";
|
|
357
|
+
discarding = false;
|
|
358
|
+
discardedChars = 0;
|
|
359
|
+
out.push(
|
|
360
|
+
...materialize(
|
|
361
|
+
parser.flush(),
|
|
362
|
+
`flush:${lineNumber}`,
|
|
363
|
+
lineNumber || 1,
|
|
364
|
+
"flush",
|
|
365
|
+
),
|
|
366
|
+
);
|
|
367
|
+
return out;
|
|
368
|
+
},
|
|
369
|
+
};
|
|
370
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
// Best-effort local HEP batching. This module owns no MCP client; the daemon
|
|
2
|
+
// injects its existing `tool` function so tests stay offline and older servers
|
|
3
|
+
// simply leave the emitter disabled through tools/list capability detection.
|
|
4
|
+
|
|
5
|
+
import { createLocalHarnessStreamAdapter } from "./local-harness-events.mjs";
|
|
6
|
+
|
|
7
|
+
export const LOCAL_HARNESS_APPEND_BATCH_MAX = 64;
|
|
8
|
+
export const LOCAL_HARNESS_OUTSTANDING_MAX = 512;
|
|
9
|
+
export const LOCAL_HARNESS_ATTEMPT_TIMEOUT_MS = 1_000;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @param {{
|
|
13
|
+
* tool: (name: string, args: Record<string, unknown>) => Promise<any>|any,
|
|
14
|
+
* runId: string,
|
|
15
|
+
* runtime: string,
|
|
16
|
+
* streamId: string,
|
|
17
|
+
* actorId: string,
|
|
18
|
+
* lane?: string,
|
|
19
|
+
* attemptTimeoutMs?: number,
|
|
20
|
+
* setTimer?: (callback: () => void, ms: number) => any,
|
|
21
|
+
* clearTimer?: (timer: any) => void,
|
|
22
|
+
* }} options
|
|
23
|
+
*/
|
|
24
|
+
export function createLocalHarnessEmitter(options) {
|
|
25
|
+
const adapter = createLocalHarnessStreamAdapter({
|
|
26
|
+
runtime: options.runtime,
|
|
27
|
+
streamId: options.streamId,
|
|
28
|
+
actorId: options.actorId,
|
|
29
|
+
lane: options.lane ?? "local",
|
|
30
|
+
});
|
|
31
|
+
let pending = [];
|
|
32
|
+
let inflight = Promise.resolve();
|
|
33
|
+
let scheduled = 0;
|
|
34
|
+
let finished = false;
|
|
35
|
+
const attemptTimeoutMs = Math.max(
|
|
36
|
+
1,
|
|
37
|
+
Number(options.attemptTimeoutMs) || LOCAL_HARNESS_ATTEMPT_TIMEOUT_MS,
|
|
38
|
+
);
|
|
39
|
+
const setTimer = options.setTimer ?? setTimeout;
|
|
40
|
+
const clearTimer = options.clearTimer ?? clearTimeout;
|
|
41
|
+
const stats = {
|
|
42
|
+
accepted: 0,
|
|
43
|
+
appended: 0,
|
|
44
|
+
replayed: 0,
|
|
45
|
+
rejected: 0,
|
|
46
|
+
dropped: 0,
|
|
47
|
+
retries: 0,
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const attempt = async (batch) => {
|
|
51
|
+
let timer;
|
|
52
|
+
try {
|
|
53
|
+
const request = Promise.resolve().then(() =>
|
|
54
|
+
options.tool("append_run_events", {
|
|
55
|
+
runId: options.runId,
|
|
56
|
+
events: batch,
|
|
57
|
+
}),
|
|
58
|
+
);
|
|
59
|
+
const deadline = new Promise((_, reject) => {
|
|
60
|
+
timer = setTimer(
|
|
61
|
+
() => reject(new Error("append_run_events timed out")),
|
|
62
|
+
attemptTimeoutMs,
|
|
63
|
+
);
|
|
64
|
+
timer?.unref?.();
|
|
65
|
+
});
|
|
66
|
+
return await Promise.race([request, deadline]);
|
|
67
|
+
} finally {
|
|
68
|
+
if (timer !== undefined) clearTimer(timer);
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const send = (batch) => {
|
|
73
|
+
scheduled += batch.length;
|
|
74
|
+
inflight = inflight
|
|
75
|
+
.then(async () => {
|
|
76
|
+
let response;
|
|
77
|
+
try {
|
|
78
|
+
response = await attempt(batch);
|
|
79
|
+
} catch {
|
|
80
|
+
// One immediate lost-response retry. Stable HEP keys make a first
|
|
81
|
+
// successful append replay instead of duplicating when its response
|
|
82
|
+
// was the part that disappeared.
|
|
83
|
+
stats.retries += 1;
|
|
84
|
+
try {
|
|
85
|
+
response = await attempt(batch);
|
|
86
|
+
} catch {
|
|
87
|
+
stats.rejected += batch.length;
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (!response || response.ok === false) {
|
|
92
|
+
stats.rejected += batch.length;
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
stats.appended += Number(response.appended) || 0;
|
|
96
|
+
stats.replayed += Number(response.replayed) || 0;
|
|
97
|
+
stats.rejected += Number(response.rejected) || 0;
|
|
98
|
+
})
|
|
99
|
+
.catch(() => {
|
|
100
|
+
// No observer failure is allowed to reach the coding process.
|
|
101
|
+
stats.rejected += batch.length;
|
|
102
|
+
})
|
|
103
|
+
.finally(() => {
|
|
104
|
+
scheduled -= batch.length;
|
|
105
|
+
});
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const accept = (events) => {
|
|
109
|
+
if (!events?.length) return;
|
|
110
|
+
const available = Math.max(
|
|
111
|
+
0,
|
|
112
|
+
LOCAL_HARNESS_OUTSTANDING_MAX - scheduled - pending.length,
|
|
113
|
+
);
|
|
114
|
+
const kept = events.slice(0, available);
|
|
115
|
+
pending.push(...kept);
|
|
116
|
+
stats.accepted += kept.length;
|
|
117
|
+
stats.dropped += events.length - kept.length;
|
|
118
|
+
while (pending.length >= LOCAL_HARNESS_APPEND_BATCH_MAX) {
|
|
119
|
+
send(pending.splice(0, LOCAL_HARNESS_APPEND_BATCH_MAX));
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
push(chunk) {
|
|
125
|
+
if (finished) return;
|
|
126
|
+
try {
|
|
127
|
+
accept(adapter.push(chunk));
|
|
128
|
+
} catch {
|
|
129
|
+
/* parser failure degrades telemetry only */
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
pushEvent(event) {
|
|
133
|
+
if (finished) return;
|
|
134
|
+
try {
|
|
135
|
+
accept(adapter.pushEvent(event));
|
|
136
|
+
} catch {
|
|
137
|
+
/* parser failure degrades telemetry only */
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
async drain() {
|
|
141
|
+
if (!finished) {
|
|
142
|
+
finished = true;
|
|
143
|
+
try {
|
|
144
|
+
accept(adapter.flush());
|
|
145
|
+
} catch {
|
|
146
|
+
/* parser failure degrades telemetry only */
|
|
147
|
+
}
|
|
148
|
+
if (pending.length) send(pending.splice(0));
|
|
149
|
+
}
|
|
150
|
+
await inflight.catch(() => {});
|
|
151
|
+
return { ...stats };
|
|
152
|
+
},
|
|
153
|
+
stats() {
|
|
154
|
+
return { ...stats };
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
}
|