memhtml 0.1.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.
@@ -0,0 +1,443 @@
1
+ import { MEMORY_TYPES, type WritableMemoryType } from "@memhtml/contracts"
2
+ import { Schema } from "effect"
3
+
4
+ /**
5
+ * What a consolidation run is allowed to return, and what a caller may act on.
6
+ *
7
+ * This module is the whole contract and holds no eve import, no network call, and no
8
+ * credential read beyond looking at `process.env` key presence. That is what lets the test
9
+ * tier decode every shape and exercise the preflight with no credentials and no server.
10
+ */
11
+
12
+ /**
13
+ * The kinds a consolidated candidate may claim, as a subset of the corpus vocabulary rather
14
+ * than a vocabulary of its own.
15
+ *
16
+ * `packages/contracts/src/types.ts:10-16` records why: three overlapping type vocabularies is
17
+ * what made the predecessor memory system's classification unanswerable. So `kind` here is a `MemoryType` value
18
+ * verbatim, and the next task writes it through the store with no translation step that could
19
+ * drift. The subset is narrower than the nine writable types because the four omitted ones
20
+ * cannot be earned from a transcript pattern:
21
+ *
22
+ * - `task` is work to do, not something observed to have happened.
23
+ * - `user_preference` is a standing instruction the user gave; inferring one from behaviour is
24
+ * how a corpus starts asserting preferences nobody stated.
25
+ * - `verdict` is a judgement this agent is not the one to pass.
26
+ * - `arc` is synthesized by the sleep cycle from many memories and is not writable at all.
27
+ */
28
+ export const CONSOLIDATION_KINDS = [
29
+ "episodic",
30
+ "semantic",
31
+ "procedural",
32
+ "agent_insight",
33
+ "error_pattern",
34
+ "precedent"
35
+ ] as const
36
+
37
+ export type ConsolidationKind = (typeof CONSOLIDATION_KINDS)[number]
38
+
39
+ /**
40
+ * Compile-time proof that every kind above is a writable corpus type. If someone adds a kind
41
+ * that `@memhtml/contracts` does not know, or one that only the sleep cycle may write, this line
42
+ * stops the build instead of the next task discovering it against a real repo.
43
+ */
44
+ const _kindsAreWritableMemoryTypes: readonly WritableMemoryType[] = CONSOLIDATION_KINDS
45
+ void _kindsAreWritableMemoryTypes
46
+
47
+ /** Ceiling on one evidence quote, so a "quote" cannot smuggle a whole transcript through. */
48
+ export const MAX_QUOTE_CHARS = 600
49
+
50
+ /** Ceiling on the prose fields, generous for a sentence and far below a transcript. */
51
+ export const MAX_CLAIM_CHARS = 300
52
+ export const MAX_GIST_CHARS = 1_500
53
+
54
+ /**
55
+ * One transcript line the candidate rests on, tied to the session it came from.
56
+ *
57
+ * Evidence is what makes the TRACE-2 bar checkable by something other than trust: a candidate
58
+ * that names a cross-session pattern has to be able to point at the lines it read it from, and
59
+ * a reviewer can go back to `sessionId` and see whether the quote is really there.
60
+ */
61
+ export class CandidateEvidence extends Schema.Class<CandidateEvidence>("CandidateEvidence")({
62
+ /**
63
+ * The session the quote was read from.
64
+ *
65
+ * Must be one of the ids this run made READABLE, which the schema cannot express, because a set
66
+ * membership over per-run values is not a schema constraint. {@link ungroundedEvidenceReason} holds
67
+ * that rule, applied by `runTurn` in `client.ts` after decode, where the reachable batch is in scope;
68
+ * a citation of an unreachable id fails the turn as a `ConsolidatorContractViolation`. All the schema
69
+ * itself asks for is that the field is present and non-empty, so a quote cannot be unattributed.
70
+ */
71
+ sessionId: Schema.String.check(Schema.isMinLength(1)),
72
+ /** A short verbatim span from that session's transcript. */
73
+ quote: Schema.String.check(Schema.isMinLength(1), Schema.isMaxLength(MAX_QUOTE_CHARS))
74
+ }) {}
75
+
76
+ /**
77
+ * One distilled candidate. Not yet a memory: the next task decides what reaches the corpus.
78
+ *
79
+ * `evidence` is `minLength(2)`, which expresses the TRACE-2 bar as a type rather than as
80
+ * prose the model may ignore. A pattern that spans lines or sessions has at least two lines
81
+ * behind it; a candidate that can only cite one is a restatement of that one line, which
82
+ * `agent/instructions.md` names as below the bar. Prose in the instructions asks for the bar,
83
+ * this refuses the turn's output without it, and the two are deliberately redundant.
84
+ */
85
+ export class CandidateMemory extends Schema.Class<CandidateMemory>("CandidateMemory")({
86
+ kind: Schema.Literals(CONSOLIDATION_KINDS),
87
+ /** One sentence stating the pattern. */
88
+ claim: Schema.String.check(Schema.isMinLength(1), Schema.isMaxLength(MAX_CLAIM_CHARS)),
89
+ /** The supporting detail: what recurs, where, and what it implies. */
90
+ gist: Schema.String.check(Schema.isMinLength(1), Schema.isMaxLength(MAX_GIST_CHARS)),
91
+ /** Tools, files, commands, packages, people the claim is about. May be empty. */
92
+ entities: Schema.Array(Schema.String.check(Schema.isMinLength(1))),
93
+ evidence: Schema.Array(CandidateEvidence).check(Schema.isMinLength(2))
94
+ }) {}
95
+
96
+ /**
97
+ * What one run produced, what it cost in model calls, and WHICH SESSIONS IT ACTUALLY REACHED.
98
+ *
99
+ * `analyzedSessionIds` is the value a caller watermarks from rather than a reporting field. It exists
100
+ * because the alternative, watermarking the batch that was ASKED about, records a transcript that
101
+ * never arrived as consolidated and never reads it again. A batch of ten where one path has been
102
+ * rotated away, or sits behind a symlink the sandbox will not follow, is not ten sessions read.
103
+ *
104
+ * The field is REQUIRED rather than optional, and that is what makes the rule structural instead of
105
+ * advisory: nothing can produce a `ConsolidationResult` without stating what it reached, so a caller
106
+ * has the honest set at hand and never has to fall back on the batch. `markSessionsConsolidated`'s
107
+ * only correct input is this set, intersected with the batch. See
108
+ * `packages/sleep/src/phases/trace-consolidation.ts`.
109
+ *
110
+ * It is the set of transcripts whose files RESOLVE AT THEIR GUEST PATH inside the sandbox's
111
+ * read-only mount, not the set the model chose to open. Those are different claims and only the
112
+ * first is checkable: nothing outside the model can prove a file was read, while a file that does
113
+ * not resolve was categorically not read. The pre-existing semantics of a watermark, "the agent saw
114
+ * this session and correctly found nothing above the bar", needs exactly the first.
115
+ */
116
+ export class ConsolidationResult extends Schema.Class<ConsolidationResult>("ConsolidationResult")({
117
+ candidates: Schema.Array(CandidateMemory),
118
+ llmCalls: Schema.Finite,
119
+ analyzedSessionIds: Schema.Array(Schema.String)
120
+ }) {}
121
+
122
+ /**
123
+ * The reason a decoded answer is not grounded in what the run made readable, or `null`.
124
+ *
125
+ * A candidate may only cite sessions THIS RUN MADE READABLE, and the schema cannot say so: a set
126
+ * membership over per-run values is not a schema constraint. So the check is a function of the
127
+ * decoded answer and the reachable ids, which is why it lives here in the contract rather than inline
128
+ * in the client. `client.ts` needs a live eve server to reach, and INV-3 keeps this app's test tier
129
+ * credential-free and server-free. Same reasoning `toJsonSchema` records for staying in this module.
130
+ *
131
+ * An id outside that set is a fabricated receipt. The id rides into the sleep phase and then into a
132
+ * commit message as `evidence <id>:`, where a reviewer's whole recourse is to go back to that session
133
+ * and check the quote is really there. An id naming a session nobody read is worse than no evidence,
134
+ * because it reads as provenance.
135
+ *
136
+ * **The whole TURN is refused, not the one candidate**, and that is a deliberate departure from the
137
+ * per-candidate isolation the sleep phase applies to its own gate. Dropping the offender here would
138
+ * be a lenient repair of a model answer, which is the posture `ConsolidationPayload`'s decode already
139
+ * refuses with `onExcessProperty: "error"`: a filtered list is indistinguishable downstream from a
140
+ * list the agent returned. And a fabricated id says the answer is not grounded in the batch handed
141
+ * over, which is a fact about the run rather than a fault in one candidate. The caller loses nothing
142
+ * it can act on: `ConsolidatorContractViolation` degrades the sleep phase to `ok` with the `_tag` in
143
+ * its detail, leaving the batch unwatermarked for the next night.
144
+ */
145
+ export const ungroundedEvidenceReason = (
146
+ candidates: ReadonlyArray<{
147
+ readonly evidence: ReadonlyArray<{ readonly sessionId: string }>
148
+ }>,
149
+ readableSessionIds: ReadonlyArray<string>
150
+ ): string | null => {
151
+ const readable = new Set(readableSessionIds)
152
+ for (const [offset, candidate] of candidates.entries()) {
153
+ const invented = candidate.evidence.find((quote) => !readable.has(quote.sessionId))
154
+ if (invented !== undefined) {
155
+ return (
156
+ `candidate ${String(offset)} cites session ${invented.sessionId}, which this run did ` +
157
+ `not make readable (${String(readable.size)} transcript(s) resolved in the sandbox)`
158
+ )
159
+ }
160
+ }
161
+ return null
162
+ }
163
+
164
+ /**
165
+ * ── The origin validation that used to live here is DELETED, with the parse it defended ──────────
166
+ *
167
+ * `loopbackOriginFrom`, `nonLoopbackOrigin`, `isLoopbackHostname`, `ANSI_ESCAPE`, and
168
+ * `URL_CANDIDATE` existed for one caller: `startServer` spawned `eve start --port 0` and read the
169
+ * bound port back off the child's stdout, so the address this process posted transcripts to was a
170
+ * string a child wrote, and validating it as loopback was the only thing standing between "eve
171
+ * printed a URL" and "the batch was posted to it".
172
+ *
173
+ * `client.ts` now chooses the port itself (`reserveLoopbackPort`) and passes it to
174
+ * `eve start --port <n>`, so the origin is composed from a constant and an integer this process got
175
+ * from the kernel. There is no untrusted string in the path any more, and nothing left to validate:
176
+ * a "defence" over a value we constructed asserts that we typed our own constant correctly.
177
+ *
178
+ * Kept as belt-and-braces it would have been WORSE than deleted, because it would have kept
179
+ * asserting a threat model that no longer holds. The deletion also costs nothing in practice:
180
+ * the readiness poll now refuses any listener that does not answer `/eve/v1/health` as eve, which
181
+ * covers the reachable case (something else on the port) more directly than a hostname check on a
182
+ * self-composed URL ever did.
183
+ *
184
+ * One measured correction to leave behind, since the old comment asserted the opposite. It claimed
185
+ * eve's piped stdout carries zero ANSI escape bytes. It does not: probed 2026-08-09 with stdout
186
+ * redirected to a file and no TTY, a failing `eve start` emitted
187
+ * `ESC[90mStopping server gracefully (5s)... Press ESC[1mCtrl+CESC[22m again…ESC[39m`. So an escape
188
+ * on that stream is real rather than theoretical. It is simply no longer on any path that decides an
189
+ * address. If anything ever parses that stream again it needs the strip, and it needs the ESC byte
190
+ * built via `String.fromCharCode` because biome's `noControlCharactersInRegex` refuses a control
191
+ * character in regex source however it is spelled.
192
+ */
193
+
194
+ /**
195
+ * The structured payload the agent is asked for.
196
+ *
197
+ * A wrapper object rather than a bare array: eve lowers this to the model's structured-output
198
+ * contract, and a top-level array leaves nowhere to say "I found nothing" that is
199
+ * distinguishable from a truncated answer. `candidates: []` is a real, readable result.
200
+ */
201
+ export class ConsolidationPayload extends Schema.Class<ConsolidationPayload>(
202
+ "ConsolidationPayload"
203
+ )({
204
+ candidates: Schema.Array(CandidateMemory)
205
+ }) {}
206
+
207
+ /**
208
+ * A JSON-safe object, structurally identical to eve's own `JsonObject`
209
+ * (node_modules/eve/dist/src/shared/json.d.ts:12).
210
+ *
211
+ * Declared here rather than imported so `contract.ts` keeps its zero-eve-import property, which
212
+ * is what lets the test tier decode every shape with no server and no credentials. TypeScript is
213
+ * structural, so the value below is assignable to eve's `outputSchema` parameter without a cast.
214
+ */
215
+ export type JsonValue = boolean | number | string | null | readonly JsonValue[] | JsonObject
216
+ export interface JsonObject {
217
+ readonly [key: string]: JsonValue
218
+ }
219
+
220
+ /**
221
+ * Derive the JSON Schema eve is handed for `outputSchema`.
222
+ *
223
+ * Deliberately a local seven lines rather than an import of `@memhtml/llm`'s `toInputSchema`
224
+ * (`packages/llm/src/structured.ts:33-38`), for two reasons. It keeps the Bedrock SDK, which
225
+ * `@memhtml/llm` pulls in for its own client, out of this app's dependency closure, and it keeps
226
+ * this app's wire shape independently derived from the same effect schema, so a change in one
227
+ * does not silently redefine the other. The `$defs` fold is the same one `structured.ts`
228
+ * documents: `toJsonSchemaDocument` hoists nested structs into `definitions` and leaves
229
+ * `$ref: "#/$defs/<name>"` behind, so the definitions go back under the root as `$defs`.
230
+ *
231
+ * The `JSON.parse(JSON.stringify(...))` normalization does two jobs, since effect types the emitted
232
+ * document loosely. It proves the value really is JSON-serializable, which matters because the
233
+ * document crosses the wire as a request body and a non-serializable member would fail at the
234
+ * boundary instead of here. It also drops `undefined`-valued keys, which are not JSON and which
235
+ * eve's own `parseJsonValue` treats as omitted.
236
+ *
237
+ * The ROOT `$ref` is then inlined, and that step changes what a consumer reads. Measured against
238
+ * effect 4.0.0-beta.102: `toJsonSchemaDocument(ConsolidationPayload)` returns a root of exactly
239
+ * `{ $ref: "#/$defs/ConsolidationPayloadJsonEncoding", $defs: {...} }`, a root with NO `type`,
240
+ * NO `properties`, and nothing at all describing an object. A nested `$ref` is well-supported
241
+ * (`packages/llm/src/structured.ts:24-27` records it verified live against Bedrock's
242
+ * `input_schema`), but a root that only points elsewhere is a different shape, and a consumer that
243
+ * reads `schema.type` to decide how to constrain the model finds `undefined`. Rather than bet the
244
+ * turn on every layer between here and the model dereferencing a root pointer, the referenced
245
+ * definition is merged into the root and dropped from `$defs`; the remaining definitions stay put
246
+ * for the nested refs that point at them.
247
+ */
248
+ export const toJsonSchema = (schema: Schema.Top): JsonObject => {
249
+ const document = Schema.toJsonSchemaDocument(schema)
250
+ const serializable = JSON.parse(
251
+ JSON.stringify({ ...document.schema, $defs: document.definitions })
252
+ ) as Record<string, JsonValue>
253
+
254
+ const { $ref: rootRef, $defs: rawDefs, ...rest } = serializable
255
+ const defs = (rawDefs ?? {}) as Record<string, JsonValue>
256
+
257
+ const rootName =
258
+ typeof rootRef === "string" && rootRef.startsWith("#/$defs/")
259
+ ? rootRef.slice("#/$defs/".length)
260
+ : null
261
+ const rootDef = rootName === null ? null : defs[rootName]
262
+
263
+ const root =
264
+ rootDef !== null &&
265
+ rootDef !== undefined &&
266
+ typeof rootDef === "object" &&
267
+ !Array.isArray(rootDef)
268
+ ? { ...rest, ...rootDef }
269
+ : { ...rest, ...(rootRef === undefined ? {} : { $ref: rootRef }) }
270
+
271
+ const remaining =
272
+ rootName === null
273
+ ? defs
274
+ : Object.fromEntries(Object.entries(defs).filter(([name]) => name !== rootName))
275
+
276
+ return (Object.keys(remaining).length === 0 ? root : { ...root, $defs: remaining }) as JsonObject
277
+ }
278
+
279
+ /** The `outputSchema` value passed on the turn. Derived once; the schema never varies. */
280
+ export const CONSOLIDATION_OUTPUT_JSON_SCHEMA = toJsonSchema(ConsolidationPayload)
281
+
282
+ /**
283
+ * ── `DEFAULT_TAIL_BYTES` is DELETED, and so is the reason it existed ──────────────────────────────
284
+ *
285
+ * It was a 256 KiB per-file cap on how much of each transcript reached the sandbox, and the cap
286
+ * bounded a mechanism that is gone: the client SEEDED transcripts, so every seeded byte was
287
+ * resident in the server process for the session's lifetime (just-bash is a pure-JS VFS holding file
288
+ * content in memory), and 256 KiB x 32 files was what bounded that at 8 MiB.
289
+ *
290
+ * Transcripts now arrive on a read-only `OverlayFs` mount that reads THROUGH to the host on demand
291
+ * (`src/mount.ts`), so nothing is resident because nothing is copied. A 37.2 MB transcript, the
292
+ * measured maximum over the live corpus, now costs whatever the model actually reads of it, and eve
293
+ * bounds each `read_file` at 2000 lines or 50 KB
294
+ * (node_modules/eve/dist/src/execution/sandbox/truncate-output.js). The budget moved from the seeding
295
+ * path to the reader, where the model spends it deliberately.
296
+ *
297
+ * Keeping the constant would have been worse than deleting it: a 256 KiB number labelled "how many
298
+ * bytes reach the sandbox" is now FALSE, and a future reader would have taken it as a live limit.
299
+ * The distribution it was measured against is still recorded (11,360 transcripts, 6.59 GB, p50
300
+ * 332 KB, p90 915 KB, p99 4.68 MB, max 37.2 MB, 2026-08-08) because
301
+ * `packages/traces/src/parse.ts:16-21` reasons about the same shape.
302
+ */
303
+
304
+ /**
305
+ * Ceiling on transcripts per run.
306
+ *
307
+ * This one SURVIVES the seeding path's removal, and its justification changes rather than
308
+ * disappearing. It no longer bounds resident bytes, since the mount does not copy, but it bounds
309
+ * how many files one agent session is asked to hold in attention, and it is the guard against a
310
+ * caller handing over five thousand sessions, which is well within what one sleep cycle could find
311
+ * unconsolidated. The sleep phase's own `TRACE_SESSIONS_PER_RUN` is lower and binds first; this is
312
+ * the client's independent backstop against a different caller.
313
+ */
314
+ export const MAX_TRANSCRIPTS_PER_RUN = 32
315
+
316
+ /** One transcript the caller wants read, named by the session it belongs to. */
317
+ export interface TranscriptRef {
318
+ readonly sessionId: string
319
+ readonly filePath: string
320
+ }
321
+
322
+ /**
323
+ * Why a run produced nothing usable. Every constructor here is something a caller can branch
324
+ * on: skip the phase, fail it, or report it.
325
+ *
326
+ * Payloads carry no transcript content. A consolidator error can be logged and reported by the
327
+ * sleep cycle, and transcript text must not ride along into a report. That is the same posture
328
+ * `packages/contracts/src/errors.ts:5-8` states for storage failures.
329
+ */
330
+
331
+ /**
332
+ * No usable credentials in the environment. Its own case, distinct from a failed call, because
333
+ * INV-3 turns on the caller being able to SKIP rather than fail: a run with no credentials is
334
+ * not a broken run, it is a run that was never possible.
335
+ */
336
+ export class ConsolidatorCredentialsMissing extends Schema.TaggedError<ConsolidatorCredentialsMissing>()(
337
+ "ConsolidatorCredentialsMissing",
338
+ {
339
+ reason: Schema.String
340
+ }
341
+ ) {}
342
+
343
+ /** The agent server could not be built, started, or reached. */
344
+ export class ConsolidatorUnavailable extends Schema.TaggedError<ConsolidatorUnavailable>()(
345
+ "ConsolidatorUnavailable",
346
+ {
347
+ reason: Schema.String
348
+ }
349
+ ) {}
350
+
351
+ /**
352
+ * The turn reached the model and did not come back with a usable answer.
353
+ *
354
+ * One type over both failure shapes the probe found, discriminated by `phase` rather than split
355
+ * into two error classes, because a caller's decision is the same for both: the run produced
356
+ * nothing. `turn` is eve's `status: "ready"` with `outcome.status: "failed"`; `invocation` is a
357
+ * top-level `status: "failed"`.
358
+ */
359
+ export class ConsolidatorRunFailed extends Schema.TaggedError<ConsolidatorRunFailed>()(
360
+ "ConsolidatorRunFailed",
361
+ {
362
+ phase: Schema.Literals(["invocation", "turn"]),
363
+ reason: Schema.String
364
+ }
365
+ ) {}
366
+
367
+ /**
368
+ * The turn settled but its structured payload is not one this contract accepts: absent when a
369
+ * schema was requested, or present and undecodable.
370
+ *
371
+ * Kept apart from {@link ConsolidatorRunFailed} because it says something different about the
372
+ * agent: it answered, and the answer broke the contract. Same posture as
373
+ * `packages/llm/src/structured.ts:52-61`: a coerced object is indistinguishable from a real one
374
+ * downstream, so nothing lenient happens here.
375
+ */
376
+ export class ConsolidatorContractViolation extends Schema.TaggedError<ConsolidatorContractViolation>()(
377
+ "ConsolidatorContractViolation",
378
+ {
379
+ reason: Schema.String
380
+ }
381
+ ) {}
382
+
383
+ /** Everything the client wrapper can fail with. */
384
+ export type ConsolidatorError =
385
+ | ConsolidatorCredentialsMissing
386
+ | ConsolidatorUnavailable
387
+ | ConsolidatorRunFailed
388
+ | ConsolidatorContractViolation
389
+
390
+ /**
391
+ * Which env vars could authenticate the Bedrock provider, in the order the provider reads them.
392
+ *
393
+ * The provider has NO default AWS credential chain, verified live in the probe: no shared
394
+ * config file, no SSO cache, no instance metadata, env vars only. So presence here is the whole
395
+ * question, and a preflight cannot be fooled by a profile that only the AWS CLI can see.
396
+ */
397
+ const BEARER_VAR = "AWS_BEARER_TOKEN_BEDROCK"
398
+ const SIGV4_VARS = ["AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY"] as const
399
+
400
+ const present = (env: Record<string, string | undefined>, name: string): boolean => {
401
+ const value = env[name]
402
+ return value !== undefined && value.trim() !== ""
403
+ }
404
+
405
+ /**
406
+ * Whether a consolidation run could authenticate at all, without making a call.
407
+ *
408
+ * Asking cheaply matters because the provider is lazy. `createAmazonBedrock` and
409
+ * `provider(modelId)` both succeed with zero credentials, and nothing fails until the first
410
+ * request, by which time a server has been built, spawned, and handed transcripts. Verified in
411
+ * the probe. So the caller checks this first and skips, which is the INV-3 groundwork: CI has
412
+ * no credentials and must stay green.
413
+ *
414
+ * Empty-string is treated as absent. A blank export is how a credential goes missing in
415
+ * practice, and `""` would authenticate nothing while reading as present.
416
+ *
417
+ * This answers "could a call be attempted", never "would it be authorized". A stale or
418
+ * unentitled key passes here and fails at the call as {@link ConsolidatorRunFailed}, which is the
419
+ * honest split, since the only way to know a key works is to use it.
420
+ */
421
+ export const hasConsolidatorCredentials = (
422
+ env: Record<string, string | undefined> = process.env
423
+ ): boolean => present(env, BEARER_VAR) || SIGV4_VARS.every((name) => present(env, name))
424
+
425
+ /**
426
+ * The message carried on {@link ConsolidatorCredentialsMissing}: which env vars would fix it.
427
+ *
428
+ * Takes no environment on purpose. It names the two accepted MECHANISMS, which never vary, and
429
+ * says nothing about which vars are currently set. A failure message is logged and reported by
430
+ * the sleep cycle, so naming the present-but-rejected variables would put credential-shaped
431
+ * details into a report for no diagnostic gain. Whether a given var is set is what
432
+ * {@link hasConsolidatorCredentials} answers.
433
+ */
434
+ export const credentialsMissingReason = (): string =>
435
+ `no Bedrock credentials in the environment: set ${BEARER_VAR}, or ${SIGV4_VARS.join(" + ")}`
436
+
437
+ /**
438
+ * Every kind is a real corpus type, restated so a reader of this file alone can see the
439
+ * relationship without opening `@memhtml/contracts`.
440
+ */
441
+ export const isConsolidationKind = (value: string): value is ConsolidationKind =>
442
+ (CONSOLIDATION_KINDS as readonly string[]).includes(value) &&
443
+ (MEMORY_TYPES as readonly string[]).includes(value)
package/src/index.ts ADDED
@@ -0,0 +1,23 @@
1
+ /**
2
+ * The consolidator app's public surface: the contract, and the client that satisfies it.
3
+ *
4
+ * A standalone app, NOT a `@memhtml/*` library. Nothing here may be reached from a `@memhtml/*` read
5
+ * path, per `.erpaval` lesson `performance/synchronous-detector-on-untrusted-write-path.md` and §8 of
6
+ * the task packet. So no barrel in `packages/*` re-exports this, and only the sleep phase that
7
+ * owns trace consolidation depends on it.
8
+ */
9
+ export * from "./client.js"
10
+ export * from "./contract.js"
11
+ /**
12
+ * The mount composition is exported because `memhtml exec` builds on it: one shared helper rather than
13
+ * the same `MountableFs` + read-only `OverlayFs` shape written twice, and this package is where
14
+ * `just-bash` is a real dependency pinned to the version eve loads.
15
+ */
16
+ export * from "./mount.js"
17
+ /**
18
+ * The run credential is exported because `agent/channels/eve.ts` imports it, and that file is compiled
19
+ * by eve into the SERVER process. That is a different build than this package's `tsc -b`, reaching
20
+ * `src/` by relative path exactly as `agent/sandbox/sandbox.ts` reaches `mount.ts`. Nothing outside
21
+ * this app consumes it: the client mints and signs, the channel verifies, and there is no third caller.
22
+ */
23
+ export * from "./run-auth.js"