@tangle-network/agent-eval 0.190.0 → 0.191.0
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 +46 -0
- package/dist/adapters/http.d.ts +2 -2
- package/dist/{agent-profile-DNV_U6QR.d.ts → agent-profile-DDfIjdyS.d.ts} +3 -3
- package/dist/{agent-profile-DNV_U6QR.d.ts.map → agent-profile-DDfIjdyS.d.ts.map} +1 -1
- package/dist/analyst/index.d.ts +11 -11
- package/dist/analyst/index.d.ts.map +1 -1
- package/dist/analyst/index.js +6 -6
- package/dist/{insight-report-CvPkEIcC.d.ts → analyze-runs-CPiKLJFs.d.ts} +73 -3
- package/dist/analyze-runs-CPiKLJFs.d.ts.map +1 -0
- package/dist/{attribute-vocabulary-DLJ6303h.d.ts → attribute-vocabulary-CcXqHODT.d.ts} +23 -12
- package/dist/attribute-vocabulary-CcXqHODT.d.ts.map +1 -0
- package/dist/{benchmark-command-TZlsuAh9.js → benchmark-command-3tZJnD82.js} +12 -11
- package/dist/benchmark-command-3tZJnD82.js.map +1 -0
- package/dist/benchmarks/index.d.ts +3 -3
- package/dist/benchmarks/index.js +3 -3
- package/dist/campaign/index.d.ts +6 -6
- package/dist/campaign/index.js +9 -7
- package/dist/{campaign-_29Qpc9Z.js → campaign-C8iWEF0M.js} +11 -9
- package/dist/{campaign-_29Qpc9Z.js.map → campaign-C8iWEF0M.js.map} +1 -1
- package/dist/{campaign-evidence-CWF8pEIk.js → campaign-evidence-BqjlTM3P.js} +11 -112
- package/dist/campaign-evidence-BqjlTM3P.js.map +1 -0
- package/dist/{chat-client-DbH06Ek6.js → chat-client-B_ItsSBw.js} +2 -2
- package/dist/{chat-client-DbH06Ek6.js.map → chat-client-B_ItsSBw.js.map} +1 -1
- package/dist/{chat-json-call-B0xTEY0I.js → chat-json-call-CXHBvUz8.js} +2 -2
- package/dist/{chat-json-call-B0xTEY0I.js.map → chat-json-call-CXHBvUz8.js.map} +1 -1
- package/dist/claim-V9Zqbq0J.js +54 -0
- package/dist/claim-V9Zqbq0J.js.map +1 -0
- package/dist/cli.js +101 -4
- package/dist/cli.js.map +1 -1
- package/dist/client-wFyDisxi.d.ts +236 -0
- package/dist/client-wFyDisxi.d.ts.map +1 -0
- package/dist/contract/index.d.ts +10 -11
- package/dist/contract/index.js +11 -141
- package/dist/contract/index.js.map +1 -1
- package/dist/{default-registry-B5HrwrPM.d.ts → default-registry-DK_9aVJf.d.ts} +6 -6
- package/dist/{default-registry-B5HrwrPM.d.ts.map → default-registry-DK_9aVJf.d.ts.map} +1 -1
- package/dist/{default-registry-16I-Bxlf.js → default-registry-xMLX160E.js} +3 -3
- package/dist/{default-registry-16I-Bxlf.js.map → default-registry-xMLX160E.js.map} +1 -1
- package/dist/{define-agent-eval-DrGfrCTZ.js → define-agent-eval-BE43YTm4.js} +27 -109
- package/dist/define-agent-eval-BE43YTm4.js.map +1 -0
- package/dist/{define-agent-eval-BWeyIpny.d.ts → define-agent-eval-P28y7mI1.d.ts} +16 -20
- package/dist/define-agent-eval-P28y7mI1.d.ts.map +1 -0
- package/dist/diagnosis/index.d.ts +16 -4
- package/dist/diagnosis/index.d.ts.map +1 -1
- package/dist/diagnosis/index.js +10 -3
- package/dist/diagnosis/index.js.map +1 -1
- package/dist/{dspy-rlm-engine-CTIGomgr.js → dspy-rlm-engine-MfpwoEaj.js} +3 -3
- package/dist/{dspy-rlm-engine-CTIGomgr.js.map → dspy-rlm-engine-MfpwoEaj.js.map} +1 -1
- package/dist/{engine-BkLCnZQX.d.ts → engine-vNiM02Ub.d.ts} +4 -4
- package/dist/{engine-BkLCnZQX.d.ts.map → engine-vNiM02Ub.d.ts.map} +1 -1
- package/dist/{eval-campaign-CMKRjWl0.js → eval-campaign-CwsBn0Gm.js} +2 -2
- package/dist/{eval-campaign-CMKRjWl0.js.map → eval-campaign-CwsBn0Gm.js.map} +1 -1
- package/dist/evaluation.d.ts +2 -2
- package/dist/{exact-types-CfpP2A4Z.d.ts → exact-types-KGxBWH4b.d.ts} +2 -2
- package/dist/{exact-types-CfpP2A4Z.d.ts.map → exact-types-KGxBWH4b.d.ts.map} +1 -1
- package/dist/experiment/index.d.ts +1 -1
- package/dist/experiment/index.js +2 -1
- package/dist/experiment/index.js.map +1 -1
- package/dist/{external-optimizer-process-Mc83UoWl.js → external-optimizer-process-xFTliuiK.js} +3 -3
- package/dist/{external-optimizer-process-Mc83UoWl.js.map → external-optimizer-process-xFTliuiK.js.map} +1 -1
- package/dist/{external-optimizer-subprocess-C0dHXH8e.js → external-optimizer-subprocess-B234V9V0.js} +5 -28
- package/dist/external-optimizer-subprocess-B234V9V0.js.map +1 -0
- package/dist/{extract-usage-BrQ8mCLX.js → extract-usage-DFATEuWW.js} +2 -2
- package/dist/{extract-usage-BrQ8mCLX.js.map → extract-usage-DFATEuWW.js.map} +1 -1
- package/dist/{feedback-trajectory-BYjh7NEM.d.ts → feedback-trajectory-BXCGTRkz.d.ts} +3 -3
- package/dist/{feedback-trajectory-BYjh7NEM.d.ts.map → feedback-trajectory-BXCGTRkz.d.ts.map} +1 -1
- package/dist/hosted/index.d.ts +66 -6
- package/dist/hosted/index.d.ts.map +1 -1
- package/dist/hosted/index.js +2 -2
- package/dist/{index-Ci0-EYzc.d.ts → index-CzfnASj_.d.ts} +18 -11
- package/dist/index-CzfnASj_.d.ts.map +1 -0
- package/dist/{index-2e_MTznj.d.ts → index-D-TN7ZRM.d.ts} +6 -6
- package/dist/{index-2e_MTznj.d.ts.map → index-D-TN7ZRM.d.ts.map} +1 -1
- package/dist/{index-BR_R1eBE.d.ts → index-DH-MWH5f.d.ts} +10 -104
- package/dist/index-DH-MWH5f.d.ts.map +1 -0
- package/dist/{index-BqDJ6fUK.d.ts → index-I4UJGRno.d.ts} +74 -2
- package/dist/index-I4UJGRno.d.ts.map +1 -0
- package/dist/index.d.ts +17 -18
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +17 -17
- package/dist/jev.d.ts +2 -2
- package/dist/{journal-CDP0Vhx_.js → journal-c3qY0SDP.js} +246 -5
- package/dist/journal-c3qY0SDP.js.map +1 -0
- package/dist/{kind-factory-ByZeXDqF.js → kind-factory-CdPPHtGZ.js} +10 -26
- package/dist/kind-factory-CdPPHtGZ.js.map +1 -0
- package/dist/ledger-core/index.d.ts +2 -2
- package/dist/ledger-core/index.js +2 -2
- package/dist/{llm-client-BqmRnibA.js → llm-client-KYQBh9C8.js} +2 -2
- package/dist/{llm-client-BqmRnibA.js.map → llm-client-KYQBh9C8.js.map} +1 -1
- package/dist/{llm-judge-bwbgDMP0.js → llm-judge-NJOHM0iA.js} +11 -1428
- package/dist/llm-judge-NJOHM0iA.js.map +1 -0
- package/dist/{matrix-CNypzP-v.d.ts → matrix-CgqdFm5N.d.ts} +2 -2
- package/dist/{matrix-CNypzP-v.d.ts.map → matrix-CgqdFm5N.d.ts.map} +1 -1
- package/dist/multishot/golden/index.d.ts +1 -1
- package/dist/multishot/index.d.ts +2 -2
- package/dist/openapi.json +1 -1
- package/dist/{otel-spans-CQY1pNiN.js → otel-spans-DS7t1t1J.js} +8 -19
- package/dist/otel-spans-DS7t1t1J.js.map +1 -0
- package/dist/otlp-attributes-D9ptWaIJ.js +53 -0
- package/dist/otlp-attributes-D9ptWaIJ.js.map +1 -0
- package/dist/pipelines/index.d.ts +1 -1
- package/dist/pipelines/index.js +1 -1
- package/dist/pipelines/index.js.map +1 -1
- package/dist/{prime-protocol-Fwvctmhm.d.ts → prime-protocol-T0i6xcAs.d.ts} +2 -2
- package/dist/{prime-protocol-Fwvctmhm.d.ts.map → prime-protocol-T0i6xcAs.d.ts.map} +1 -1
- package/dist/{produced-state-zSaGDC_Q.js → produced-state-CY-L7CqS.js} +4 -4
- package/dist/{produced-state-zSaGDC_Q.js.map → produced-state-CY-L7CqS.js.map} +1 -1
- package/dist/{raw-provider-sink-DIRCZjP_.js → raw-provider-sink-DYh4Bszw.js} +2 -2
- package/dist/{raw-provider-sink-DIRCZjP_.js.map → raw-provider-sink-DYh4Bszw.js.map} +1 -1
- package/dist/{redact-8fX8-uKr.js → redact-B7rt9Zot.js} +117 -6
- package/dist/redact-B7rt9Zot.js.map +1 -0
- package/dist/{redact-Du3WKUOP.d.ts → redact-CopuMxll.d.ts} +2 -2
- package/dist/{redact-Du3WKUOP.d.ts.map → redact-CopuMxll.d.ts.map} +1 -1
- package/dist/{registry-mAQVtuLx.d.ts → registry-Bu3lNole.d.ts} +3 -3
- package/dist/{registry-mAQVtuLx.d.ts.map → registry-Bu3lNole.d.ts.map} +1 -1
- package/dist/rl.d.ts +1 -1
- package/dist/rl.js +2 -2
- package/dist/{run-record-DnMFyg_g.js → run-record-tNnlQIrD.js} +2 -10
- package/dist/{run-record-DnMFyg_g.js.map → run-record-tNnlQIrD.js.map} +1 -1
- package/dist/search-ledger-CQ8Uy4FK.js +1451 -0
- package/dist/search-ledger-CQ8Uy4FK.js.map +1 -0
- package/dist/search-ledger-file-EvdDZRhv.js +27 -0
- package/dist/search-ledger-file-EvdDZRhv.js.map +1 -0
- package/dist/search-shipper-C55V7HE3.js +781 -0
- package/dist/search-shipper-C55V7HE3.js.map +1 -0
- package/dist/{semantic-concept-judge-BY0me-Jq.js → semantic-concept-judge-Bt7bXAAo.js} +6 -6
- package/dist/{semantic-concept-judge-BY0me-Jq.js.map → semantic-concept-judge-Bt7bXAAo.js.map} +1 -1
- package/dist/{server-DnJCDLf8.js → server-DtetGGoA.js} +2 -2
- package/dist/{server-DnJCDLf8.js.map → server-DtetGGoA.js.map} +1 -1
- package/dist/{skillopt-optimization-method-CfnQEjgT.js → skillopt-optimization-method-DwO3TOAV.js} +7 -6
- package/dist/{skillopt-optimization-method-CfnQEjgT.js.map → skillopt-optimization-method-DwO3TOAV.js.map} +1 -1
- package/dist/{spans-D1RftP_w.d.ts → spans-BWtjk_9c.d.ts} +2 -2
- package/dist/{spans-D1RftP_w.d.ts.map → spans-BWtjk_9c.d.ts.map} +1 -1
- package/dist/{statistical-heldout-CLpjYffJ.d.ts → statistical-heldout-BED3wBtX.d.ts} +2 -2
- package/dist/{statistical-heldout-CLpjYffJ.d.ts.map → statistical-heldout-BED3wBtX.d.ts.map} +1 -1
- package/dist/{store-otlp-84DH0OEH.js → store-otlp-BPowgPm6.js} +2 -2
- package/dist/{store-otlp-84DH0OEH.js.map → store-otlp-BPowgPm6.js.map} +1 -1
- package/dist/{store-tool-spans-DSbYpl7f.js → store-tool-spans-BcC_5ef-.js} +6 -6
- package/dist/{store-tool-spans-DSbYpl7f.js.map → store-tool-spans-BcC_5ef-.js.map} +1 -1
- package/dist/{store-tool-spans-BBXs8hDP.d.ts → store-tool-spans-Cfh9XUCm.d.ts} +5 -9
- package/dist/store-tool-spans-Cfh9XUCm.d.ts.map +1 -0
- package/dist/{task-failure-attributes-B34TiMBe.js → task-failure-attributes-CEqEfuIY.js} +17 -9
- package/dist/task-failure-attributes-CEqEfuIY.js.map +1 -0
- package/dist/{tool-groups-W7MJobX_.d.ts → tool-groups-D-mkeN0N.d.ts} +3 -3
- package/dist/tool-groups-D-mkeN0N.d.ts.map +1 -0
- package/dist/trace-attributes.d.ts +1 -1
- package/dist/trace-attributes.js +23 -75
- package/dist/trace-attributes.js.map +1 -1
- package/dist/trace-repair/index.d.ts +1 -1
- package/dist/traces.d.ts +12 -19
- package/dist/traces.d.ts.map +1 -1
- package/dist/traces.js +18 -18
- package/dist/traces.js.map +1 -1
- package/dist/{types-BvmWNjjs.d.ts → types-BmQRs661.d.ts} +2 -2
- package/dist/{types-BvmWNjjs.d.ts.map → types-BmQRs661.d.ts.map} +1 -1
- package/dist/{types-Bay1dh2o.d.ts → types-CeDfCRXp.d.ts} +11 -16
- package/dist/types-CeDfCRXp.d.ts.map +1 -0
- package/dist/{types-Cvqy7asY.d.ts → types-ClK-sHs8.d.ts} +2 -2
- package/dist/{types-Cvqy7asY.d.ts.map → types-ClK-sHs8.d.ts.map} +1 -1
- package/dist/wire/index.d.ts +1 -1
- package/dist/wire/index.js +1 -1
- package/docs/hosted-ingest-spec.md +92 -133
- package/docs/public-api.md +58 -43
- package/docs/search-ledger.md +17 -0
- package/package.json +2 -2
- package/dist/analyze-runs-BvK4-wSs.d.ts +0 -75
- package/dist/analyze-runs-BvK4-wSs.d.ts.map +0 -1
- package/dist/attribute-vocabulary-DLJ6303h.d.ts.map +0 -1
- package/dist/benchmark-command-TZlsuAh9.js.map +0 -1
- package/dist/campaign-evidence-CWF8pEIk.js.map +0 -1
- package/dist/client-BhEyAdfe.d.ts +0 -202
- package/dist/client-BhEyAdfe.d.ts.map +0 -1
- package/dist/client-QwJpsNYU.js +0 -655
- package/dist/client-QwJpsNYU.js.map +0 -1
- package/dist/define-agent-eval-BWeyIpny.d.ts.map +0 -1
- package/dist/define-agent-eval-DrGfrCTZ.js.map +0 -1
- package/dist/external-optimizer-subprocess-C0dHXH8e.js.map +0 -1
- package/dist/index-BR_R1eBE.d.ts.map +0 -1
- package/dist/index-BqDJ6fUK.d.ts.map +0 -1
- package/dist/index-Ci0-EYzc.d.ts.map +0 -1
- package/dist/insight-report-CvPkEIcC.d.ts.map +0 -1
- package/dist/journal-CDP0Vhx_.js.map +0 -1
- package/dist/kind-factory-ByZeXDqF.js.map +0 -1
- package/dist/llm-judge-bwbgDMP0.js.map +0 -1
- package/dist/otel-spans-CQY1pNiN.js.map +0 -1
- package/dist/otlp-attributes-BpC_blLF.js +0 -83
- package/dist/otlp-attributes-BpC_blLF.js.map +0 -1
- package/dist/redact-8fX8-uKr.js.map +0 -1
- package/dist/store-tool-spans-BBXs8hDP.d.ts.map +0 -1
- package/dist/task-failure-attributes-B34TiMBe.js.map +0 -1
- package/dist/tool-groups-W7MJobX_.d.ts.map +0 -1
- package/dist/types-Bay1dh2o.d.ts.map +0 -1
|
@@ -0,0 +1,781 @@
|
|
|
1
|
+
import { o as SearchLedgerIntegrityError } from "./search-ledger-file-EvdDZRhv.js";
|
|
2
|
+
import { r as parseSearchLedgerLine } from "./search-ledger-CQ8Uy4FK.js";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { open, readFile } from "node:fs/promises";
|
|
7
|
+
//#region src/hosted/types.ts
|
|
8
|
+
/**
|
|
9
|
+
* # Hosted-tier wire format: the shapes every orchestrator (ours, a partner's
|
|
10
|
+
* self-hosted one, a future open implementation) must accept.
|
|
11
|
+
*
|
|
12
|
+
* This package implements exactly one wire version. Servers reject every
|
|
13
|
+
* other version instead of translating old payloads.
|
|
14
|
+
*
|
|
15
|
+
* The wire carries two streams in one transport:
|
|
16
|
+
*
|
|
17
|
+
* 1. **Search ledgers** (`/v1/search-blobs/*`, `/v1/ingest/search-ledger`).
|
|
18
|
+
* A producer ships a search's hash-chained ledger lines and the blobs
|
|
19
|
+
* they name; `./search-ledger-wire.ts` owns those shapes.
|
|
20
|
+
*
|
|
21
|
+
* 2. **Trace spans** (`POST /v1/ingest/traces`). Standard OTLP-shaped
|
|
22
|
+
* spans with a few additional attributes so the orchestrator can
|
|
23
|
+
* pivot from a search cell to its underlying execution. Compatible
|
|
24
|
+
* with any OTel collector.
|
|
25
|
+
*
|
|
26
|
+
* Every endpoint is authenticated with a bearer token and a tenant id
|
|
27
|
+
* header. Tenants isolate everything downstream of ingest; no tenant
|
|
28
|
+
* ever sees another tenant's data.
|
|
29
|
+
*/
|
|
30
|
+
const HOSTED_WIRE_VERSION = "2026-07-24.v1";
|
|
31
|
+
//#endregion
|
|
32
|
+
//#region src/hosted/schemas.ts
|
|
33
|
+
const finiteNumber = z.number().finite();
|
|
34
|
+
const nonNegativeInteger = z.number().int().nonnegative();
|
|
35
|
+
const nonEmptyString = z.string().min(1).refine((value) => value.trim().length > 0, "must not be blank");
|
|
36
|
+
const attributeValue = z.union([
|
|
37
|
+
z.string(),
|
|
38
|
+
finiteNumber,
|
|
39
|
+
z.boolean()
|
|
40
|
+
]);
|
|
41
|
+
const attributes = z.record(z.string(), attributeValue);
|
|
42
|
+
const UINT64_MAX = 18446744073709551615n;
|
|
43
|
+
const UnixNanoTimestampSchema = z.string().regex(/^(0|[1-9][0-9]*)$/, "expected an unsigned base-10 integer string").refine((value) => BigInt(value) <= UINT64_MAX, "must fit in an unsigned 64-bit integer");
|
|
44
|
+
const TraceSpanEventEntrySchema = z.object({
|
|
45
|
+
timeUnixNano: UnixNanoTimestampSchema,
|
|
46
|
+
name: nonEmptyString,
|
|
47
|
+
attributes: attributes.optional()
|
|
48
|
+
}).strict();
|
|
49
|
+
const TraceSpanEventSchema = z.object({
|
|
50
|
+
traceId: nonEmptyString,
|
|
51
|
+
spanId: nonEmptyString,
|
|
52
|
+
parentSpanId: nonEmptyString.optional(),
|
|
53
|
+
name: nonEmptyString,
|
|
54
|
+
startTimeUnixNano: UnixNanoTimestampSchema,
|
|
55
|
+
endTimeUnixNano: UnixNanoTimestampSchema,
|
|
56
|
+
attributes,
|
|
57
|
+
events: z.array(TraceSpanEventEntrySchema).optional(),
|
|
58
|
+
status: z.object({
|
|
59
|
+
code: z.enum([
|
|
60
|
+
"OK",
|
|
61
|
+
"ERROR",
|
|
62
|
+
"UNSET"
|
|
63
|
+
]),
|
|
64
|
+
message: z.string().optional()
|
|
65
|
+
}).strict().optional(),
|
|
66
|
+
"tangle.runId": nonEmptyString.optional(),
|
|
67
|
+
"tangle.generation": nonNegativeInteger.optional(),
|
|
68
|
+
"tangle.cellId": nonEmptyString.optional(),
|
|
69
|
+
"tangle.scenarioId": nonEmptyString.optional()
|
|
70
|
+
}).strict().refine((span) => BigInt(span.endTimeUnixNano) >= BigInt(span.startTimeUnixNano), {
|
|
71
|
+
path: ["endTimeUnixNano"],
|
|
72
|
+
message: "endTimeUnixNano must be greater than or equal to startTimeUnixNano"
|
|
73
|
+
});
|
|
74
|
+
z.object({
|
|
75
|
+
wireVersion: z.literal(HOSTED_WIRE_VERSION),
|
|
76
|
+
spans: z.array(z.unknown())
|
|
77
|
+
}).strict();
|
|
78
|
+
const IngestTracesRequestSchema = z.object({
|
|
79
|
+
wireVersion: z.literal(HOSTED_WIRE_VERSION),
|
|
80
|
+
spans: z.array(TraceSpanEventSchema)
|
|
81
|
+
}).strict();
|
|
82
|
+
const IngestResponseSchema = z.object({
|
|
83
|
+
accepted: nonNegativeInteger,
|
|
84
|
+
rejected: z.array(z.object({
|
|
85
|
+
index: nonNegativeInteger,
|
|
86
|
+
reason: nonEmptyString
|
|
87
|
+
}).strict())
|
|
88
|
+
}).strict();
|
|
89
|
+
//#endregion
|
|
90
|
+
//#region src/hosted/search-ledger-wire.ts
|
|
91
|
+
/**
|
|
92
|
+
* The search-ledger wire: how a producer ships a search ledger to a hosted
|
|
93
|
+
* store, and the rule a store applies to each batch.
|
|
94
|
+
*
|
|
95
|
+
* A producer ships three things, in this order:
|
|
96
|
+
*
|
|
97
|
+
* 1. Blobs: `PUT /v1/search-blobs/<sha256 hex>` with the exact bytes an
|
|
98
|
+
* entry's artifact ref names. The store re-hashes them; the call is
|
|
99
|
+
* idempotent by content.
|
|
100
|
+
* 2. Head: `GET /v1/ingest/search-ledger/<searchId>/head` tells a restarted
|
|
101
|
+
* producer where the store's copy of the chain ends.
|
|
102
|
+
* 3. Entries: `POST /v1/ingest/search-ledger` with canonical ledger lines,
|
|
103
|
+
* starting at `fromSequence`. The store verifies every line, links it to
|
|
104
|
+
* its stored head, and answers with its new head.
|
|
105
|
+
*
|
|
106
|
+
* The lines are the ledger's own bytes, so producer and store hash identical
|
|
107
|
+
* input. A line below the store's head with an equal hash is a no-op; a
|
|
108
|
+
* different hash is a fork (`409 chain_conflict`); a batch that starts past
|
|
109
|
+
* the head is a gap (`409 sequence_gap`). Both 409s carry the store's head,
|
|
110
|
+
* so a producer resends from there or stops on a fork.
|
|
111
|
+
*/
|
|
112
|
+
/** At most this many lines in one ingest request. */
|
|
113
|
+
const SEARCH_LEDGER_BATCH_MAX_LINES = 1e3;
|
|
114
|
+
/** At most this many UTF-8 bytes across one request's lines. A single line
|
|
115
|
+
* larger than this cannot be shipped. */
|
|
116
|
+
const SEARCH_LEDGER_BATCH_MAX_BYTES = 1048576;
|
|
117
|
+
const SEARCH_LEDGER_INGEST_PATH = "/v1/ingest/search-ledger";
|
|
118
|
+
function searchLedgerHeadPath(searchId) {
|
|
119
|
+
return `${SEARCH_LEDGER_INGEST_PATH}/${encodeURIComponent(searchId)}/head`;
|
|
120
|
+
}
|
|
121
|
+
/** Blobs are addressed by the hex digest alone. */
|
|
122
|
+
function searchBlobPath(sha256) {
|
|
123
|
+
return `/v1/search-blobs/${sha256.slice(7)}`;
|
|
124
|
+
}
|
|
125
|
+
const HASH = z.custom((value) => typeof value === "string" && /^sha256:[a-f0-9]{64}$/.test(value), "expected sha256:<64 lowercase hex characters>");
|
|
126
|
+
const SEARCH_ID = z.string().min(1).refine((value) => value.trim() === value, "must not contain surrounding whitespace");
|
|
127
|
+
const SEQUENCE = z.number().int().nonnegative().safe();
|
|
128
|
+
/** What the run is for: an optimization, or an eval (a one-node search). */
|
|
129
|
+
const SearchRunKindSchema = z.enum(["optimization", "eval"]);
|
|
130
|
+
/** Where a store's copy of one search's chain ends. `headHash` is null exactly
|
|
131
|
+
* when the store holds no entry. */
|
|
132
|
+
const SearchLedgerHeadSchema = z.object({
|
|
133
|
+
searchId: SEARCH_ID,
|
|
134
|
+
nextSequence: SEQUENCE,
|
|
135
|
+
headHash: HASH.nullable()
|
|
136
|
+
}).strict().refine((head) => head.nextSequence === 0 === (head.headHash === null), {
|
|
137
|
+
message: "headHash must be null exactly when nextSequence is 0",
|
|
138
|
+
path: ["headHash"]
|
|
139
|
+
});
|
|
140
|
+
const IngestSearchLedgerRequestSchema = z.object({
|
|
141
|
+
wireVersion: z.literal(HOSTED_WIRE_VERSION),
|
|
142
|
+
searchId: SEARCH_ID,
|
|
143
|
+
runKind: SearchRunKindSchema,
|
|
144
|
+
fromSequence: SEQUENCE,
|
|
145
|
+
lines: z.array(z.string().min(1)).min(1).max(SEARCH_LEDGER_BATCH_MAX_LINES)
|
|
146
|
+
}).strict().superRefine((request, ctx) => {
|
|
147
|
+
const bytes = searchLedgerLinesByteLength(request.lines);
|
|
148
|
+
if (bytes > 1048576) ctx.addIssue({
|
|
149
|
+
code: "custom",
|
|
150
|
+
path: ["lines"],
|
|
151
|
+
message: `lines hold ${bytes} bytes; at most ${SEARCH_LEDGER_BATCH_MAX_BYTES} per request`
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
/** The body of a `409`: a fork or a gap, with the store's head. */
|
|
155
|
+
const SearchLedgerConflictSchema = z.object({
|
|
156
|
+
error: z.enum(["chain_conflict", "sequence_gap"]),
|
|
157
|
+
message: z.string().min(1),
|
|
158
|
+
head: SearchLedgerHeadSchema
|
|
159
|
+
}).strict();
|
|
160
|
+
/** What a store kept of one blob. `masked` and `withheld` come from the
|
|
161
|
+
* store's own redaction scan or the tenant's content posture. */
|
|
162
|
+
const SearchBlobPutResponseSchema = z.object({
|
|
163
|
+
sha256: HASH,
|
|
164
|
+
byteLength: SEQUENCE,
|
|
165
|
+
state: z.enum([
|
|
166
|
+
"stored",
|
|
167
|
+
"masked",
|
|
168
|
+
"withheld"
|
|
169
|
+
])
|
|
170
|
+
}).strict();
|
|
171
|
+
const UTF8 = new TextEncoder();
|
|
172
|
+
function searchLedgerLinesByteLength(lines) {
|
|
173
|
+
let bytes = 0;
|
|
174
|
+
for (const line of lines) bytes += UTF8.encode(line).byteLength;
|
|
175
|
+
return bytes;
|
|
176
|
+
}
|
|
177
|
+
/** Every blob an event names, once each. Ship these before the event. */
|
|
178
|
+
function searchLedgerArtifactRefs(event) {
|
|
179
|
+
const refs = [...event.artifacts];
|
|
180
|
+
switch (event.kind) {
|
|
181
|
+
case "node-registered":
|
|
182
|
+
refs.push(event.artifact, ...event.surfaces.map((surface) => surface.artifact));
|
|
183
|
+
break;
|
|
184
|
+
case "edge-recorded":
|
|
185
|
+
for (const ref of [event.rationale, ...event.diffs]) if ("uri" in ref) refs.push(ref);
|
|
186
|
+
break;
|
|
187
|
+
case "cell-settled":
|
|
188
|
+
for (const evidence of event.surfaceEvidence) refs.push(...evidence.evidence);
|
|
189
|
+
break;
|
|
190
|
+
default: break;
|
|
191
|
+
}
|
|
192
|
+
const unique = /* @__PURE__ */ new Map();
|
|
193
|
+
for (const ref of refs) if (!unique.has(ref.sha256)) unique.set(ref.sha256, ref);
|
|
194
|
+
return [...unique.values()];
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Apply the wire's chain rule to one batch. Each line is verified in isolation
|
|
198
|
+
* (`parseSearchLedgerLine`) and linked to the stored head. A malformed line
|
|
199
|
+
* throws `SearchLedgerIntegrityError`, which a store answers with 422; the
|
|
200
|
+
* state machine (`SearchState`) runs after admission, on `entries`.
|
|
201
|
+
*/
|
|
202
|
+
function admitSearchLedgerBatch(input) {
|
|
203
|
+
const { request, head } = input;
|
|
204
|
+
if (request.searchId !== head.searchId) throw new Error(`head is for search ${head.searchId}, request is for ${request.searchId}`);
|
|
205
|
+
if (request.fromSequence > head.nextSequence) return conflict("sequence_gap", `batch starts at sequence ${request.fromSequence}; the store expects ${head.nextSequence}`, head);
|
|
206
|
+
const entries = [];
|
|
207
|
+
const lines = [];
|
|
208
|
+
let previousHash = head.headHash;
|
|
209
|
+
for (let index = 0; index < request.lines.length; index++) {
|
|
210
|
+
const sequence = request.fromSequence + index;
|
|
211
|
+
const line = request.lines[index];
|
|
212
|
+
const entry = parseSearchLedgerLine(line, request.searchId, {
|
|
213
|
+
path: `search ${request.searchId}`,
|
|
214
|
+
line: sequence + 1
|
|
215
|
+
});
|
|
216
|
+
if (entry.sequence !== sequence) throw new SearchLedgerIntegrityError(`line ${index} of the batch holds sequence ${entry.sequence}, expected ${sequence}`);
|
|
217
|
+
if (sequence < head.nextSequence) {
|
|
218
|
+
if (input.storedEntryHash(sequence) !== entry.entryHash) return conflict("chain_conflict", `sequence ${sequence} differs from the stored entry; the ledger forked`, head);
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
if (entry.previousHash !== previousHash) {
|
|
222
|
+
if (sequence === head.nextSequence) return conflict("chain_conflict", `sequence ${sequence} does not extend the stored head; the ledger forked`, head);
|
|
223
|
+
throw new SearchLedgerIntegrityError(`sequence ${sequence} does not extend sequence ${sequence - 1} in the same batch`);
|
|
224
|
+
}
|
|
225
|
+
entries.push(entry);
|
|
226
|
+
lines.push(line);
|
|
227
|
+
previousHash = entry.entryHash;
|
|
228
|
+
}
|
|
229
|
+
const last = entries.at(-1);
|
|
230
|
+
return {
|
|
231
|
+
status: "accepted",
|
|
232
|
+
entries,
|
|
233
|
+
lines,
|
|
234
|
+
head: last ? {
|
|
235
|
+
searchId: head.searchId,
|
|
236
|
+
nextSequence: last.sequence + 1,
|
|
237
|
+
headHash: last.entryHash
|
|
238
|
+
} : head
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
function conflict(error, message, head) {
|
|
242
|
+
return {
|
|
243
|
+
status: "conflict",
|
|
244
|
+
conflict: {
|
|
245
|
+
error,
|
|
246
|
+
message,
|
|
247
|
+
head
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
//#endregion
|
|
252
|
+
//#region src/hosted/client.ts
|
|
253
|
+
/** A request the server refused, after any retries. */
|
|
254
|
+
var HostedRequestError = class extends Error {
|
|
255
|
+
status;
|
|
256
|
+
body;
|
|
257
|
+
constructor(url, status, body) {
|
|
258
|
+
super(`hosted ${url} failed (${status}): ${body.slice(0, 500)}`);
|
|
259
|
+
this.name = "HostedRequestError";
|
|
260
|
+
this.status = status;
|
|
261
|
+
this.body = body;
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
const MAX_IDEMPOTENCY_KEY_LENGTH = 256;
|
|
265
|
+
function sleep(ms, signal) {
|
|
266
|
+
return new Promise((resolve, reject) => {
|
|
267
|
+
if (signal?.aborted) return reject(signal.reason);
|
|
268
|
+
const timer = setTimeout(() => {
|
|
269
|
+
signal?.removeEventListener("abort", onAbort);
|
|
270
|
+
resolve();
|
|
271
|
+
}, ms);
|
|
272
|
+
const onAbort = () => {
|
|
273
|
+
clearTimeout(timer);
|
|
274
|
+
reject(signal.reason);
|
|
275
|
+
};
|
|
276
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
/** `Retry-After` as milliseconds: delay-seconds or an HTTP date. Null when
|
|
280
|
+
* absent or unreadable, so the caller falls back to its own backoff. */
|
|
281
|
+
function retryAfterMs(value, now = Date.now()) {
|
|
282
|
+
if (value === null) return null;
|
|
283
|
+
const trimmed = value.trim();
|
|
284
|
+
if (/^\d+$/.test(trimmed)) return Number(trimmed) * 1e3;
|
|
285
|
+
const at = Date.parse(trimmed);
|
|
286
|
+
return Number.isFinite(at) ? Math.max(0, at - now) : null;
|
|
287
|
+
}
|
|
288
|
+
function backoffMs(attempt) {
|
|
289
|
+
return 2 ** attempt * 200 + Math.random() * 200;
|
|
290
|
+
}
|
|
291
|
+
function normalizeHostedBase(endpoint) {
|
|
292
|
+
return endpoint.trim().replace(/\/+$/, "").replace(/\/v1$/, "");
|
|
293
|
+
}
|
|
294
|
+
function resolveIdempotencyKey(key) {
|
|
295
|
+
const resolved = key ?? globalThis.crypto.randomUUID();
|
|
296
|
+
if (resolved.trim().length === 0) throw new Error("idempotency key must not be blank");
|
|
297
|
+
if (resolved.length > MAX_IDEMPOTENCY_KEY_LENGTH) throw new Error(`idempotency key must be at most ${MAX_IDEMPOTENCY_KEY_LENGTH} characters`);
|
|
298
|
+
return resolved;
|
|
299
|
+
}
|
|
300
|
+
function isRetryable(status) {
|
|
301
|
+
return status >= 500 || status === 408 || status === 429;
|
|
302
|
+
}
|
|
303
|
+
async function send(tenant, request) {
|
|
304
|
+
const timeoutMs = tenant.timeoutMs ?? 3e4;
|
|
305
|
+
const maxRetries = tenant.retries ?? 2;
|
|
306
|
+
const f = tenant.fetchImpl ?? ((...args) => fetch(...args));
|
|
307
|
+
const url = `${normalizeHostedBase(tenant.endpoint)}${request.path}`;
|
|
308
|
+
const headers = {
|
|
309
|
+
authorization: `Bearer ${tenant.apiKey}`,
|
|
310
|
+
"x-tangle-tenant-id": tenant.tenantId,
|
|
311
|
+
"x-tangle-wire-version": HOSTED_WIRE_VERSION
|
|
312
|
+
};
|
|
313
|
+
if (request.method !== "GET") {
|
|
314
|
+
headers["idempotency-key"] = resolveIdempotencyKey(request.idempotencyKey);
|
|
315
|
+
headers["content-type"] = request.contentType ?? "application/json";
|
|
316
|
+
}
|
|
317
|
+
let lastError;
|
|
318
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
319
|
+
const timeout = AbortSignal.timeout(timeoutMs);
|
|
320
|
+
const signal = request.signal ? AbortSignal.any([request.signal, timeout]) : timeout;
|
|
321
|
+
let res;
|
|
322
|
+
try {
|
|
323
|
+
res = await f(url, {
|
|
324
|
+
method: request.method,
|
|
325
|
+
headers,
|
|
326
|
+
...request.body === void 0 ? {} : { body: request.body },
|
|
327
|
+
signal
|
|
328
|
+
});
|
|
329
|
+
} catch (err) {
|
|
330
|
+
if (request.signal?.aborted) throw err;
|
|
331
|
+
lastError = err;
|
|
332
|
+
if (attempt === maxRetries) throw err;
|
|
333
|
+
await sleep(backoffMs(attempt), request.signal);
|
|
334
|
+
continue;
|
|
335
|
+
}
|
|
336
|
+
if (!res.ok && !request.expected?.includes(res.status)) {
|
|
337
|
+
const text = await res.text().catch(() => "");
|
|
338
|
+
const error = new HostedRequestError(url, res.status, text);
|
|
339
|
+
if (!isRetryable(res.status) || attempt === maxRetries) throw error;
|
|
340
|
+
lastError = error;
|
|
341
|
+
await sleep(retryAfterMs(res.headers.get("retry-after")) ?? backoffMs(attempt), request.signal);
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
try {
|
|
345
|
+
return {
|
|
346
|
+
status: res.status,
|
|
347
|
+
json: await res.json(),
|
|
348
|
+
url
|
|
349
|
+
};
|
|
350
|
+
} catch (error) {
|
|
351
|
+
throw new Error(`hosted ${url} returned invalid JSON`, { cause: error });
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
throw lastError ?? /* @__PURE__ */ new Error("hosted request exhausted retries");
|
|
355
|
+
}
|
|
356
|
+
function parseResponse(schema, value, url) {
|
|
357
|
+
const parsed = schema.safeParse(value);
|
|
358
|
+
if (parsed.success) return parsed.data;
|
|
359
|
+
const reason = parsed.error.issues.map((issue) => `${issue.path.map(String).join(".") || "value"}: ${issue.message}`).join("; ");
|
|
360
|
+
throw new Error(`hosted ${url} returned an invalid response: ${reason}`);
|
|
361
|
+
}
|
|
362
|
+
function createHostedClient(tenant) {
|
|
363
|
+
if (normalizeHostedBase(tenant.endpoint).length === 0) throw new Error("endpoint is required");
|
|
364
|
+
if (tenant.apiKey.trim().length === 0) throw new Error("apiKey is required");
|
|
365
|
+
if (tenant.tenantId.trim().length === 0) throw new Error("tenantId is required");
|
|
366
|
+
if (tenant.timeoutMs !== void 0 && (!Number.isFinite(tenant.timeoutMs) || tenant.timeoutMs <= 0)) throw new Error("timeoutMs must be greater than 0");
|
|
367
|
+
if (tenant.retries !== void 0 && (!Number.isInteger(tenant.retries) || tenant.retries < 0)) throw new Error("retries must be a non-negative integer");
|
|
368
|
+
return {
|
|
369
|
+
tenant,
|
|
370
|
+
wireVersion: HOSTED_WIRE_VERSION,
|
|
371
|
+
async ingestTraces(spans, idempotencyKey) {
|
|
372
|
+
const body = IngestTracesRequestSchema.parse({
|
|
373
|
+
wireVersion: HOSTED_WIRE_VERSION,
|
|
374
|
+
spans
|
|
375
|
+
});
|
|
376
|
+
const res = await send(tenant, {
|
|
377
|
+
method: "POST",
|
|
378
|
+
path: "/v1/ingest/traces",
|
|
379
|
+
body: JSON.stringify(body),
|
|
380
|
+
idempotencyKey
|
|
381
|
+
});
|
|
382
|
+
return parseResponse(IngestResponseSchema, res.json, res.url);
|
|
383
|
+
},
|
|
384
|
+
async searchLedgerHead(searchId, signal) {
|
|
385
|
+
const res = await send(tenant, {
|
|
386
|
+
method: "GET",
|
|
387
|
+
path: searchLedgerHeadPath(searchId),
|
|
388
|
+
signal
|
|
389
|
+
});
|
|
390
|
+
const head = parseResponse(SearchLedgerHeadSchema, res.json, res.url);
|
|
391
|
+
if (head.searchId !== searchId) throw new Error(`hosted ${res.url} returned the head of search ${head.searchId}`);
|
|
392
|
+
return head;
|
|
393
|
+
},
|
|
394
|
+
async ingestSearchLedger(request, signal) {
|
|
395
|
+
const body = IngestSearchLedgerRequestSchema.parse(request);
|
|
396
|
+
const last = body.lines.at(-1);
|
|
397
|
+
const res = await send(tenant, {
|
|
398
|
+
method: "POST",
|
|
399
|
+
path: SEARCH_LEDGER_INGEST_PATH,
|
|
400
|
+
body: JSON.stringify(body),
|
|
401
|
+
idempotencyKey: `search-ledger:${await sha256Hex(`${body.searchId}\n${body.runKind}\n${body.fromSequence}\n${body.lines.length}\n${last}`)}`,
|
|
402
|
+
signal,
|
|
403
|
+
expected: [409]
|
|
404
|
+
});
|
|
405
|
+
if (res.status === 409) return {
|
|
406
|
+
status: "conflict",
|
|
407
|
+
conflict: parseResponse(SearchLedgerConflictSchema, res.json, res.url)
|
|
408
|
+
};
|
|
409
|
+
return {
|
|
410
|
+
status: "accepted",
|
|
411
|
+
head: parseResponse(SearchLedgerHeadSchema, res.json, res.url)
|
|
412
|
+
};
|
|
413
|
+
},
|
|
414
|
+
async putSearchBlob(sha256, bytes, contentType, signal) {
|
|
415
|
+
const res = await send(tenant, {
|
|
416
|
+
method: "PUT",
|
|
417
|
+
path: searchBlobPath(sha256),
|
|
418
|
+
body: bytes,
|
|
419
|
+
contentType,
|
|
420
|
+
idempotencyKey: `search-blob:${sha256}`,
|
|
421
|
+
signal
|
|
422
|
+
});
|
|
423
|
+
const stored = parseResponse(SearchBlobPutResponseSchema, res.json, res.url);
|
|
424
|
+
if (stored.sha256 !== sha256) throw new Error(`hosted ${res.url} acknowledged blob ${stored.sha256}, sent ${sha256}`);
|
|
425
|
+
return stored;
|
|
426
|
+
}
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
async function sha256Hex(text) {
|
|
430
|
+
const digest = await globalThis.crypto.subtle.digest("SHA-256", new TextEncoder().encode(text));
|
|
431
|
+
return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
432
|
+
}
|
|
433
|
+
/**
|
|
434
|
+
* Build a {@link HostedTenant} from env, or `undefined` when ingest is not
|
|
435
|
+
* configured, so a product wires it unconditionally and it stays off until the
|
|
436
|
+
* env is set. Env precedence:
|
|
437
|
+
* - endpoint: `TANGLE_INGEST_URL` → `TANGLE_ORCHESTRATOR_URL`
|
|
438
|
+
* - apiKey: `TANGLE_INGEST_API_KEY` → `TANGLE_API_KEY`
|
|
439
|
+
* - tenantId: `TANGLE_TENANT_ID`
|
|
440
|
+
* A trailing slash on the endpoint is stripped. `overrides` win over env.
|
|
441
|
+
*/
|
|
442
|
+
function hostedTenantFromEnv(overrides = {}) {
|
|
443
|
+
const env = overrides.env ?? process.env;
|
|
444
|
+
const endpoint = (overrides.endpoint ?? env.TANGLE_INGEST_URL ?? env.TANGLE_ORCHESTRATOR_URL)?.trim();
|
|
445
|
+
const apiKey = (overrides.apiKey ?? env.TANGLE_INGEST_API_KEY ?? env.TANGLE_API_KEY)?.trim();
|
|
446
|
+
const tenantId = (overrides.tenantId ?? env.TANGLE_TENANT_ID)?.trim();
|
|
447
|
+
if (!endpoint || !apiKey || !tenantId) return void 0;
|
|
448
|
+
const tenant = {
|
|
449
|
+
endpoint: endpoint.replace(/\/+$/, ""),
|
|
450
|
+
apiKey,
|
|
451
|
+
tenantId
|
|
452
|
+
};
|
|
453
|
+
if (overrides.fetchImpl) tenant.fetchImpl = overrides.fetchImpl;
|
|
454
|
+
if (overrides.timeoutMs !== void 0) tenant.timeoutMs = overrides.timeoutMs;
|
|
455
|
+
if (overrides.retries !== void 0) tenant.retries = overrides.retries;
|
|
456
|
+
return tenant;
|
|
457
|
+
}
|
|
458
|
+
/** {@link createHostedClient} over {@link hostedTenantFromEnv}; `undefined`
|
|
459
|
+
* when ingest is not configured. */
|
|
460
|
+
function hostedClientFromEnv(overrides = {}) {
|
|
461
|
+
const tenant = hostedTenantFromEnv(overrides);
|
|
462
|
+
return tenant ? createHostedClient(tenant) : void 0;
|
|
463
|
+
}
|
|
464
|
+
//#endregion
|
|
465
|
+
//#region src/hosted/search-shipper.ts
|
|
466
|
+
/**
|
|
467
|
+
* Ship a search ledger to a hosted store while the search runs, or after.
|
|
468
|
+
*
|
|
469
|
+
* The local ledger is the source of truth and the search never waits on the
|
|
470
|
+
* network: the shipper reads the ledger file the recorder already made
|
|
471
|
+
* durable, uploads the blobs each entry names, then posts the entries. It
|
|
472
|
+
* starts from the store's head, so a restarted process, a lost response, or a
|
|
473
|
+
* second shipper continues where the store's chain ends instead of resending
|
|
474
|
+
* the search. A store that holds a different chain for the same search stops
|
|
475
|
+
* the shipper with `SearchShipConflictError`; nothing is overwritten.
|
|
476
|
+
*/
|
|
477
|
+
/** The store holds a different chain for this search. */
|
|
478
|
+
var SearchShipConflictError = class extends Error {
|
|
479
|
+
conflict;
|
|
480
|
+
constructor(conflict) {
|
|
481
|
+
super(`search ${conflict.head.searchId}: ${conflict.error}: ${conflict.message} (store head ${conflict.head.nextSequence} ${conflict.head.headHash ?? "empty"})`);
|
|
482
|
+
this.name = "SearchShipConflictError";
|
|
483
|
+
this.conflict = conflict;
|
|
484
|
+
}
|
|
485
|
+
};
|
|
486
|
+
/** Ship every complete line of the ledger file now, then return. */
|
|
487
|
+
async function shipSearchLedger(options) {
|
|
488
|
+
return new SearchLedgerTail(options).ship();
|
|
489
|
+
}
|
|
490
|
+
/**
|
|
491
|
+
* Tail the ledger: ship new lines every `intervalMs` (default 2 s) until
|
|
492
|
+
* `stop()`. A transient failure is kept on `lastError` and retried on the next
|
|
493
|
+
* tick; a conflict stops tailing and is thrown by `stop()`.
|
|
494
|
+
*/
|
|
495
|
+
function startSearchShipper(options) {
|
|
496
|
+
const tail = new SearchLedgerTail(options);
|
|
497
|
+
const intervalMs = options.intervalMs ?? 2e3;
|
|
498
|
+
const stopping = new AbortController();
|
|
499
|
+
let lastError = null;
|
|
500
|
+
let conflict = null;
|
|
501
|
+
let running = Promise.resolve();
|
|
502
|
+
const tick = () => {
|
|
503
|
+
running = running.then(async () => {
|
|
504
|
+
if (conflict) return;
|
|
505
|
+
try {
|
|
506
|
+
await tail.ship();
|
|
507
|
+
lastError = null;
|
|
508
|
+
} catch (error) {
|
|
509
|
+
if (error instanceof SearchShipConflictError) conflict = error;
|
|
510
|
+
else lastError = error instanceof Error ? error : new Error(String(error));
|
|
511
|
+
}
|
|
512
|
+
});
|
|
513
|
+
return running;
|
|
514
|
+
};
|
|
515
|
+
const loop = (async () => {
|
|
516
|
+
while (!stopping.signal.aborted && !conflict) {
|
|
517
|
+
await tick();
|
|
518
|
+
await new Promise((resolve) => {
|
|
519
|
+
const timer = setTimeout(resolve, intervalMs);
|
|
520
|
+
stopping.signal.addEventListener("abort", () => {
|
|
521
|
+
clearTimeout(timer);
|
|
522
|
+
resolve();
|
|
523
|
+
}, { once: true });
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
})();
|
|
527
|
+
return {
|
|
528
|
+
get lastError() {
|
|
529
|
+
return lastError;
|
|
530
|
+
},
|
|
531
|
+
async flush() {
|
|
532
|
+
await tick();
|
|
533
|
+
if (conflict) throw conflict;
|
|
534
|
+
if (lastError) throw lastError;
|
|
535
|
+
return tail.result();
|
|
536
|
+
},
|
|
537
|
+
async stop() {
|
|
538
|
+
stopping.abort();
|
|
539
|
+
await loop;
|
|
540
|
+
await running;
|
|
541
|
+
if (conflict) throw conflict;
|
|
542
|
+
return tail.ship();
|
|
543
|
+
}
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
/** One full batch of lines with their newlines, plus a byte to see where a
|
|
547
|
+
* maximal line ends. */
|
|
548
|
+
const READ_CHUNK_BYTES = 1049577;
|
|
549
|
+
var SearchLedgerTail = class {
|
|
550
|
+
client;
|
|
551
|
+
options;
|
|
552
|
+
content;
|
|
553
|
+
/** Where the store's chain ends, and the byte offset of that line locally. */
|
|
554
|
+
head = null;
|
|
555
|
+
offset = 0;
|
|
556
|
+
localLines = 0;
|
|
557
|
+
uploaded = /* @__PURE__ */ new Set();
|
|
558
|
+
missing = /* @__PURE__ */ new Map();
|
|
559
|
+
counts = {
|
|
560
|
+
linesPosted: 0,
|
|
561
|
+
batches: 0,
|
|
562
|
+
gapResends: 0,
|
|
563
|
+
uploaded: 0,
|
|
564
|
+
stored: 0,
|
|
565
|
+
masked: 0,
|
|
566
|
+
withheld: 0
|
|
567
|
+
};
|
|
568
|
+
constructor(options) {
|
|
569
|
+
this.options = options;
|
|
570
|
+
this.content = options.content ?? "full";
|
|
571
|
+
this.client = createHostedClient(options.tenant);
|
|
572
|
+
}
|
|
573
|
+
result() {
|
|
574
|
+
const { linesPosted, batches, gapResends, uploaded, stored, masked, withheld } = this.counts;
|
|
575
|
+
return {
|
|
576
|
+
searchId: this.options.ledger.searchId,
|
|
577
|
+
head: this.head ?? {
|
|
578
|
+
searchId: this.options.ledger.searchId,
|
|
579
|
+
nextSequence: 0,
|
|
580
|
+
headHash: null
|
|
581
|
+
},
|
|
582
|
+
localLines: this.localLines,
|
|
583
|
+
linesPosted,
|
|
584
|
+
batches,
|
|
585
|
+
gapResends,
|
|
586
|
+
blobs: {
|
|
587
|
+
content: this.content,
|
|
588
|
+
uploaded,
|
|
589
|
+
stored,
|
|
590
|
+
masked,
|
|
591
|
+
withheld,
|
|
592
|
+
missing: [...this.missing.values()]
|
|
593
|
+
}
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
async ship() {
|
|
597
|
+
const { signal } = this.options;
|
|
598
|
+
if (this.head === null) await this.syncTo(await this.client.searchLedgerHead(this.options.ledger.searchId, signal));
|
|
599
|
+
for (;;) {
|
|
600
|
+
const lines = await this.readChunk(this.offset, this.head);
|
|
601
|
+
if (lines.length === 0) return this.result();
|
|
602
|
+
const batch = takeBatch(lines);
|
|
603
|
+
if (this.content === "full") await this.uploadBlobs(batch);
|
|
604
|
+
const fromSequence = this.head.nextSequence;
|
|
605
|
+
const outcome = await this.client.ingestSearchLedger({
|
|
606
|
+
wireVersion: HOSTED_WIRE_VERSION,
|
|
607
|
+
searchId: this.options.ledger.searchId,
|
|
608
|
+
runKind: this.options.runKind,
|
|
609
|
+
fromSequence,
|
|
610
|
+
lines: batch.map((line) => line.text)
|
|
611
|
+
}, signal);
|
|
612
|
+
this.counts.batches++;
|
|
613
|
+
this.counts.linesPosted += batch.length;
|
|
614
|
+
if (outcome.status === "conflict") {
|
|
615
|
+
if (outcome.conflict.error === "chain_conflict") throw new SearchShipConflictError(outcome.conflict);
|
|
616
|
+
this.counts.gapResends++;
|
|
617
|
+
await this.syncTo(outcome.conflict.head);
|
|
618
|
+
continue;
|
|
619
|
+
}
|
|
620
|
+
const last = batch.at(-1).entry;
|
|
621
|
+
if (outcome.head.nextSequence < last.sequence + 1 || outcome.head.nextSequence === last.sequence + 1 && outcome.head.headHash !== last.entryHash) throw new Error(`store acknowledged sequence ${fromSequence}..${last.sequence} with head ${outcome.head.nextSequence} ${outcome.head.headHash}`);
|
|
622
|
+
await this.syncTo(outcome.head);
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
/** Move the cursor to the store's head, checking that the local ledger holds
|
|
626
|
+
* the same entry there. A different entry is a fork; a store ahead of the
|
|
627
|
+
* local file holds lines this ledger never wrote. */
|
|
628
|
+
async syncTo(head) {
|
|
629
|
+
if (head.searchId !== this.options.ledger.searchId) throw new Error(`store answered for search ${head.searchId}`);
|
|
630
|
+
const fork = (message) => new SearchShipConflictError({
|
|
631
|
+
error: "chain_conflict",
|
|
632
|
+
message,
|
|
633
|
+
head
|
|
634
|
+
});
|
|
635
|
+
const empty = {
|
|
636
|
+
searchId: head.searchId,
|
|
637
|
+
nextSequence: 0,
|
|
638
|
+
headHash: null
|
|
639
|
+
};
|
|
640
|
+
const forward = this.head !== null && head.nextSequence >= this.head.nextSequence;
|
|
641
|
+
let offset = forward ? this.offset : 0;
|
|
642
|
+
let cursor = forward ? this.head : empty;
|
|
643
|
+
while (cursor.nextSequence < head.nextSequence) {
|
|
644
|
+
const lines = await this.readChunk(offset, cursor);
|
|
645
|
+
if (lines.length === 0) throw fork(`the store holds ${head.nextSequence} entries; the local ledger holds ${cursor.nextSequence}`);
|
|
646
|
+
const take = Math.min(lines.length, head.nextSequence - cursor.nextSequence);
|
|
647
|
+
for (let index = 0; index < take; index++) offset += lines[index].bytes;
|
|
648
|
+
const last = lines[take - 1].entry;
|
|
649
|
+
cursor = {
|
|
650
|
+
searchId: head.searchId,
|
|
651
|
+
nextSequence: last.sequence + 1,
|
|
652
|
+
headHash: last.entryHash
|
|
653
|
+
};
|
|
654
|
+
}
|
|
655
|
+
if (cursor.headHash !== head.headHash) throw fork(`the store's entry ${head.nextSequence - 1} differs from the local ledger's`);
|
|
656
|
+
this.offset = offset;
|
|
657
|
+
this.head = head;
|
|
658
|
+
}
|
|
659
|
+
/** The complete lines in one read chunk from `offset`, checked to continue
|
|
660
|
+
* the chain at `head`. A chunk holds at least one full batch. */
|
|
661
|
+
async readChunk(offset, head) {
|
|
662
|
+
const bytes = await readRange(this.options.ledger.path, offset, READ_CHUNK_BYTES);
|
|
663
|
+
const lines = [];
|
|
664
|
+
let start = 0;
|
|
665
|
+
let expectedSequence = head.nextSequence;
|
|
666
|
+
let previousHash = head.headHash;
|
|
667
|
+
for (;;) {
|
|
668
|
+
const newline = bytes.indexOf(10, start);
|
|
669
|
+
if (newline < 0) break;
|
|
670
|
+
const text = bytes.toString("utf8", start, newline);
|
|
671
|
+
const entry = JSON.parse(text);
|
|
672
|
+
if (entry.searchId !== this.options.ledger.searchId || entry.sequence !== expectedSequence || entry.previousHash !== previousHash) throw new Error(`${this.options.ledger.path} line ${expectedSequence + 1} does not continue the chain at sequence ${expectedSequence}`);
|
|
673
|
+
lines.push({
|
|
674
|
+
text,
|
|
675
|
+
entry,
|
|
676
|
+
bytes: newline + 1 - start
|
|
677
|
+
});
|
|
678
|
+
expectedSequence++;
|
|
679
|
+
previousHash = entry.entryHash;
|
|
680
|
+
start = newline + 1;
|
|
681
|
+
}
|
|
682
|
+
if (lines.length === 0 && bytes.byteLength === READ_CHUNK_BYTES) throw new Error(`${this.options.ledger.path} line ${expectedSequence + 1} is longer than ${SEARCH_LEDGER_BATCH_MAX_BYTES} bytes; one request carries at most that`);
|
|
683
|
+
this.localLines = Math.max(this.localLines, expectedSequence);
|
|
684
|
+
return lines;
|
|
685
|
+
}
|
|
686
|
+
async uploadBlobs(batch) {
|
|
687
|
+
for (const line of batch) for (const ref of searchLedgerArtifactRefs(line.entry.event)) {
|
|
688
|
+
if (this.uploaded.has(ref.sha256) || this.missing.has(ref.sha256)) continue;
|
|
689
|
+
const bytes = await readBlob(ref.uri);
|
|
690
|
+
if (typeof bytes === "string") {
|
|
691
|
+
this.missing.set(ref.sha256, {
|
|
692
|
+
sha256: ref.sha256,
|
|
693
|
+
uri: ref.uri,
|
|
694
|
+
reason: bytes
|
|
695
|
+
});
|
|
696
|
+
continue;
|
|
697
|
+
}
|
|
698
|
+
const digest = `sha256:${createHash("sha256").update(bytes).digest("hex")}`;
|
|
699
|
+
if (digest !== ref.sha256 || bytes.byteLength !== ref.byteLength) {
|
|
700
|
+
this.missing.set(ref.sha256, {
|
|
701
|
+
sha256: ref.sha256,
|
|
702
|
+
uri: ref.uri,
|
|
703
|
+
reason: `local bytes are ${digest} (${bytes.byteLength} bytes), not the ref's ${ref.sha256} (${ref.byteLength} bytes)`
|
|
704
|
+
});
|
|
705
|
+
continue;
|
|
706
|
+
}
|
|
707
|
+
const stored = await this.client.putSearchBlob(ref.sha256, new Uint8Array(bytes), ref.uri.endsWith(".json") ? "application/json" : "application/octet-stream", this.options.signal);
|
|
708
|
+
this.uploaded.add(ref.sha256);
|
|
709
|
+
this.counts.uploaded++;
|
|
710
|
+
this.counts[stored.state]++;
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
};
|
|
714
|
+
/** The longest prefix of `lines` within one request's limits. */
|
|
715
|
+
function takeBatch(lines) {
|
|
716
|
+
const batch = [];
|
|
717
|
+
let bytes = 0;
|
|
718
|
+
for (const line of lines) {
|
|
719
|
+
const size = line.bytes - 1;
|
|
720
|
+
if (size > 1048576) throw new Error(`ledger line ${line.entry.sequence} holds ${size} bytes; one request carries at most ${SEARCH_LEDGER_BATCH_MAX_BYTES}`);
|
|
721
|
+
if (batch.length === 1e3 || bytes + size > 1048576) break;
|
|
722
|
+
batch.push(line);
|
|
723
|
+
bytes += size;
|
|
724
|
+
}
|
|
725
|
+
return batch;
|
|
726
|
+
}
|
|
727
|
+
/** Up to `limit` bytes of the file from `offset`; empty before the recorder
|
|
728
|
+
* created the file. */
|
|
729
|
+
async function readRange(path, offset, limit) {
|
|
730
|
+
let handle;
|
|
731
|
+
try {
|
|
732
|
+
handle = await open(path, "r");
|
|
733
|
+
} catch (error) {
|
|
734
|
+
if (error.code === "ENOENT" && offset === 0) return Buffer.alloc(0);
|
|
735
|
+
throw error;
|
|
736
|
+
}
|
|
737
|
+
try {
|
|
738
|
+
const { size } = await handle.stat();
|
|
739
|
+
if (size < offset) throw new Error(`${path} shrank below byte ${offset}`);
|
|
740
|
+
const bytes = Buffer.alloc(Math.min(limit, size - offset));
|
|
741
|
+
let read = 0;
|
|
742
|
+
while (read < bytes.byteLength) {
|
|
743
|
+
const { bytesRead } = await handle.read(bytes, read, bytes.byteLength - read, offset + read);
|
|
744
|
+
if (bytesRead === 0) break;
|
|
745
|
+
read += bytesRead;
|
|
746
|
+
}
|
|
747
|
+
return bytes.subarray(0, read);
|
|
748
|
+
} finally {
|
|
749
|
+
await handle.close();
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
/** The bytes at a `file:` ref, or why there are none. */
|
|
753
|
+
async function readBlob(uri) {
|
|
754
|
+
if (!uri.startsWith("file:")) return `the shipper reads only file: refs, not ${uri.split(":")[0]}:`;
|
|
755
|
+
try {
|
|
756
|
+
return await readFile(fileURLToPath(uri));
|
|
757
|
+
} catch (error) {
|
|
758
|
+
return `unreadable: ${error.code ?? String(error)}`;
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
/**
|
|
762
|
+
* Run a ship that must never fail its caller, such as the one a loop runs at
|
|
763
|
+
* its end. A failure, a store short of the local ledger, or a blob that did
|
|
764
|
+
* not upload is logged with the command that resumes it.
|
|
765
|
+
*/
|
|
766
|
+
async function shipBestEffort(ship, ledgerPath) {
|
|
767
|
+
const resume = `agent-eval search ship ${ledgerPath}`;
|
|
768
|
+
try {
|
|
769
|
+
const shipped = await ship();
|
|
770
|
+
if (shipped.head.nextSequence < shipped.localLines) console.warn(`[agent-eval] search ${shipped.searchId}: the store holds ${shipped.head.nextSequence} of ${shipped.localLines} ledger entries; resume with ${resume}`);
|
|
771
|
+
if (shipped.blobs.missing.length > 0) console.warn(`[agent-eval] search ${shipped.searchId}: ${shipped.blobs.missing.length} blob(s) were not uploaded, first: ${shipped.blobs.missing[0].reason}`);
|
|
772
|
+
return shipped;
|
|
773
|
+
} catch (error) {
|
|
774
|
+
console.warn(`[agent-eval] search ledger ship failed (continuing; resume with ${resume}): ${error instanceof Error ? error.message : String(error)}`);
|
|
775
|
+
return;
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
//#endregion
|
|
779
|
+
export { TraceSpanEventSchema as C, IngestTracesRequestSchema as S, HOSTED_WIRE_VERSION as T, admitSearchLedgerBatch as _, HostedRequestError as a, searchLedgerHeadPath as b, hostedTenantFromEnv as c, SEARCH_LEDGER_BATCH_MAX_LINES as d, SEARCH_LEDGER_INGEST_PATH as f, SearchRunKindSchema as g, SearchLedgerHeadSchema as h, startSearchShipper as i, IngestSearchLedgerRequestSchema as l, SearchLedgerConflictSchema as m, shipBestEffort as n, createHostedClient as o, SearchBlobPutResponseSchema as p, shipSearchLedger as r, hostedClientFromEnv as s, SearchShipConflictError as t, SEARCH_LEDGER_BATCH_MAX_BYTES as u, searchBlobPath as v, UnixNanoTimestampSchema as w, IngestResponseSchema as x, searchLedgerArtifactRefs as y };
|
|
780
|
+
|
|
781
|
+
//# sourceMappingURL=search-shipper-C55V7HE3.js.map
|