memhtml 0.4.0 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{dist-D73gfqLc.mjs → dist-DgKlozi6.mjs} +378 -75
- package/dist/dist-DgKlozi6.mjs.map +1 -0
- package/dist/memhtml-mcp.mjs +2 -2
- package/dist/memhtml-mcp.mjs.map +1 -1
- package/dist/memhtml.mjs +2 -2
- package/dist/memhtml.mjs.map +1 -1
- package/package.json +1 -1
- package/dist/dist-D73gfqLc.mjs.map +0 -1
|
@@ -8267,12 +8267,23 @@ const EmbeddingsLive = Layer.effect(Embeddings, Effect.gen(function* () {
|
|
|
8267
8267
|
//#endregion
|
|
8268
8268
|
//#region packages/llm/dist/models.js
|
|
8269
8269
|
/**
|
|
8270
|
-
* The
|
|
8271
|
-
*
|
|
8272
|
-
*
|
|
8273
|
-
*
|
|
8270
|
+
* The models the sleep phases run on, and the wire rules that differ between them.
|
|
8271
|
+
*
|
|
8272
|
+
* Two providers, two call shapes, one decode. The Anthropic lane speaks the native
|
|
8273
|
+
* Messages body with a forced `emit` tool; the OpenAI lane speaks the chat-completions
|
|
8274
|
+
* body with `response_format: {type: "json_schema", strict: true}`. The OpenAI lane
|
|
8275
|
+
* exists for exactly one property the Anthropic lane cannot offer on Bedrock today:
|
|
8276
|
+
* constrained decoding, which makes an off-schema structured answer impossible at
|
|
8277
|
+
* generation time instead of repaired after (probed live 2026-08-22 — the Claude 5
|
|
8278
|
+
* models reject `strict` and `output_config.format` on every Bedrock surface, while
|
|
8279
|
+
* `global.openai.gpt-5.6-sol` honors strict JSON schema on InvokeModel). Both lanes
|
|
8280
|
+
* converge on the same response shape before `decodeToolInput`, so every phase and
|
|
8281
|
+
* every test sees one contract.
|
|
8282
|
+
*/
|
|
8283
|
+
/**
|
|
8284
|
+
* Reasoning effort. The Anthropic lane passes it as `output_config.effort`, the OpenAI
|
|
8285
|
+
* lane as `reasoning_effort`; both accept all four values (sol probed live 2026-08-22).
|
|
8274
8286
|
*/
|
|
8275
|
-
/** Reasoning effort, passed as `output_config.effort`. Accepted by all three models. */
|
|
8276
8287
|
const Effort = Schema.Literals([
|
|
8277
8288
|
"low",
|
|
8278
8289
|
"medium",
|
|
@@ -8282,27 +8293,39 @@ const Effort = Schema.Literals([
|
|
|
8282
8293
|
const ModelKey = Schema.Literals([
|
|
8283
8294
|
"sonnet-5",
|
|
8284
8295
|
"opus-5",
|
|
8285
|
-
"fable-5"
|
|
8296
|
+
"fable-5",
|
|
8297
|
+
"gpt-5.6-sol"
|
|
8286
8298
|
]);
|
|
8287
8299
|
/**
|
|
8288
8300
|
* Bedrock ids use the `global.` inference profiles, which makes them reachable from a
|
|
8289
|
-
* single region without provisioning per-region throughput.
|
|
8301
|
+
* single region without provisioning per-region throughput. The OpenAI models REQUIRE
|
|
8302
|
+
* the profile: the bare `openai.gpt-5.6-*` ids reject on-demand invocation outright
|
|
8303
|
+
* (probed live 2026-08-22).
|
|
8290
8304
|
*/
|
|
8291
8305
|
const MODELS = [
|
|
8292
8306
|
{
|
|
8293
8307
|
key: "sonnet-5",
|
|
8294
8308
|
label: "Claude Sonnet 5",
|
|
8295
|
-
modelId: "global.anthropic.claude-sonnet-5"
|
|
8309
|
+
modelId: "global.anthropic.claude-sonnet-5",
|
|
8310
|
+
provider: "anthropic"
|
|
8296
8311
|
},
|
|
8297
8312
|
{
|
|
8298
8313
|
key: "opus-5",
|
|
8299
8314
|
label: "Claude Opus 5",
|
|
8300
|
-
modelId: "global.anthropic.claude-opus-5"
|
|
8315
|
+
modelId: "global.anthropic.claude-opus-5",
|
|
8316
|
+
provider: "anthropic"
|
|
8301
8317
|
},
|
|
8302
8318
|
{
|
|
8303
8319
|
key: "fable-5",
|
|
8304
8320
|
label: "Claude Fable 5",
|
|
8305
|
-
modelId: "global.anthropic.claude-fable-5"
|
|
8321
|
+
modelId: "global.anthropic.claude-fable-5",
|
|
8322
|
+
provider: "anthropic"
|
|
8323
|
+
},
|
|
8324
|
+
{
|
|
8325
|
+
key: "gpt-5.6-sol",
|
|
8326
|
+
label: "GPT-5.6 Sol",
|
|
8327
|
+
modelId: "global.openai.gpt-5.6-sol",
|
|
8328
|
+
provider: "openai"
|
|
8306
8329
|
}
|
|
8307
8330
|
];
|
|
8308
8331
|
const BY_KEY = new Map(MODELS.map((model) => [model.key, model]));
|
|
@@ -8317,12 +8340,13 @@ const modelByKey = (key) => {
|
|
|
8317
8340
|
return found;
|
|
8318
8341
|
};
|
|
8319
8342
|
/**
|
|
8320
|
-
* The `thinking` object per model. Opus 5 and Fable 5 take `{type: "adaptive"}`
|
|
8321
|
-
* adaptive-only). Sonnet 5 reasons unconditionally and takes NO thinking key.
|
|
8322
|
-
* to Sonnet 5 raises a validation error instead of being ignored.
|
|
8343
|
+
* The `thinking` object per Anthropic model. Opus 5 and Fable 5 take `{type: "adaptive"}`
|
|
8344
|
+
* (Fable is adaptive-only). Sonnet 5 reasons unconditionally and takes NO thinking key.
|
|
8345
|
+
* Sending one to Sonnet 5 raises a validation error instead of being ignored. The OpenAI
|
|
8346
|
+
* lane never consults this: its reasoning dial is `reasoning_effort` alone.
|
|
8323
8347
|
*
|
|
8324
|
-
* Verified live 2026-08-02: all three accept this shape alongside a forced
|
|
8325
|
-
* so structured output and adaptive thinking compose.
|
|
8348
|
+
* Verified live 2026-08-02: all three Claude models accept this shape alongside a forced
|
|
8349
|
+
* `tool_choice`, so structured output and adaptive thinking compose.
|
|
8326
8350
|
*/
|
|
8327
8351
|
const thinkingFor = (key) => key === "opus-5" || key === "fable-5" ? { type: "adaptive" } : null;
|
|
8328
8352
|
|
|
@@ -8337,6 +8361,13 @@ const thinkingFor = (key) => key === "opus-5" || key === "fable-5" ? { type: "ad
|
|
|
8337
8361
|
* default for an omitted field, and no accepted extra key. Downstream code cannot tell a
|
|
8338
8362
|
* coerced object from a real one, and the phases that consume these objects archive and
|
|
8339
8363
|
* rewrite files.
|
|
8364
|
+
*
|
|
8365
|
+
* ONE repair is the exception, because it recovers the payload the model meant rather than
|
|
8366
|
+
* inventing one: a top-level field the schema declares as an array or object sometimes
|
|
8367
|
+
* arrives double-encoded as a JSON STRING. That string is parsed once and the SAME strict
|
|
8368
|
+
* decode re-runs on the result, so nothing an off-schema answer carries can slip through —
|
|
8369
|
+
* a payload the repair cannot make satisfy the schema still fails with the original
|
|
8370
|
+
* violation. See {@link decodeToolInput}.
|
|
8340
8371
|
*/
|
|
8341
8372
|
/** Cap on the raw payload carried on a violation, so a runaway response cannot bloat it. */
|
|
8342
8373
|
const MAX_RAW = 800;
|
|
@@ -8371,6 +8402,50 @@ const preview = (payload) => {
|
|
|
8371
8402
|
})();
|
|
8372
8403
|
return rendered.length <= 800 ? rendered : `${rendered.slice(0, 800)}…`;
|
|
8373
8404
|
};
|
|
8405
|
+
/** True when a property's derived JSON schema declares a container: an array, an object, or a
|
|
8406
|
+
* `$ref` (every hoisted definition is a struct). A string-typed property is NOT a container,
|
|
8407
|
+
* which is what keeps a field that legitimately holds JSON-looking text out of the repair. */
|
|
8408
|
+
const expectsContainer = (property) => {
|
|
8409
|
+
if (typeof property !== "object" || property === null) return false;
|
|
8410
|
+
const record = property;
|
|
8411
|
+
return typeof record.$ref === "string" || record.type === "array" || record.type === "object";
|
|
8412
|
+
};
|
|
8413
|
+
/**
|
|
8414
|
+
* Undo ONE level of JSON-string double-encoding on the top-level fields of a tool payload.
|
|
8415
|
+
*
|
|
8416
|
+
* The shape this repairs was observed on the wire: a field the schema declares as an array
|
|
8417
|
+
* arrives as `"{\"groups\":[…]}"` — the whole answer serialized as a string under its own
|
|
8418
|
+
* key — or as the array itself serialized. So a string sitting where the derived
|
|
8419
|
+
* `input_schema` declares a container is parsed once; when the parsed value is an object
|
|
8420
|
+
* carrying the SAME key, the value at that key is taken, otherwise the parsed value stands.
|
|
8421
|
+
*
|
|
8422
|
+
* Returns `undefined` when there is nothing to repair: no field qualified, or no parse
|
|
8423
|
+
* succeeded. The caller then reports the ORIGINAL violation, and a repaired payload still
|
|
8424
|
+
* re-runs the same strict decode, so this never widens what the schema accepts.
|
|
8425
|
+
*/
|
|
8426
|
+
const unwrapDoubleEncoded = (schema, input) => {
|
|
8427
|
+
if (typeof input !== "object" || input === null || Array.isArray(input)) return void 0;
|
|
8428
|
+
const properties = toInputSchema(schema).properties;
|
|
8429
|
+
if (properties === void 0) return void 0;
|
|
8430
|
+
let repairedAField = false;
|
|
8431
|
+
const repaired = {};
|
|
8432
|
+
for (const [key, received] of Object.entries(input)) {
|
|
8433
|
+
repaired[key] = received;
|
|
8434
|
+
if (typeof received !== "string" || !expectsContainer(properties[key])) continue;
|
|
8435
|
+
const parsed = (() => {
|
|
8436
|
+
try {
|
|
8437
|
+
return { value: JSON.parse(received) };
|
|
8438
|
+
} catch {
|
|
8439
|
+
return;
|
|
8440
|
+
}
|
|
8441
|
+
})();
|
|
8442
|
+
if (parsed === void 0) continue;
|
|
8443
|
+
const wrapper = typeof parsed.value === "object" && parsed.value !== null && !Array.isArray(parsed.value) ? parsed.value : void 0;
|
|
8444
|
+
repaired[key] = wrapper !== void 0 && key in wrapper ? wrapper[key] : parsed.value;
|
|
8445
|
+
repairedAField = true;
|
|
8446
|
+
}
|
|
8447
|
+
return repairedAField ? repaired : void 0;
|
|
8448
|
+
};
|
|
8374
8449
|
/**
|
|
8375
8450
|
* Decode a forced-tool payload against its schema.
|
|
8376
8451
|
*
|
|
@@ -8379,13 +8454,29 @@ const preview = (payload) => {
|
|
|
8379
8454
|
* would let a model answer a schema next to the one it was given and have the extra field
|
|
8380
8455
|
* vanish. croq's judge rules out the same drift by enumerating its allowed keys.
|
|
8381
8456
|
*
|
|
8457
|
+
* One failure shape is repaired before the violation is constructed: a top-level container
|
|
8458
|
+
* field double-encoded as a JSON string ({@link unwrapDoubleEncoded}). The repaired payload
|
|
8459
|
+
* goes through the SAME strict decode, and a repair that still does not satisfy the schema
|
|
8460
|
+
* reports the original payload's violation, so the repair cannot mask a genuinely off-schema
|
|
8461
|
+
* answer.
|
|
8462
|
+
*
|
|
8382
8463
|
* `undefined` input means the model produced no `emit` call at all. That is the same class
|
|
8383
8464
|
* of failure as a malformed one, and the reason text names it so a caller can tell the two
|
|
8384
8465
|
* apart in a log without a second error type.
|
|
8385
8466
|
*/
|
|
8386
8467
|
const decodeToolInput = (schema, input) => input === void 0 ? Effect.fail(LlmContractViolation.make({ reason: "model returned no tool_use block for the forced tool" })) : Effect.gen(function* () {
|
|
8387
|
-
const
|
|
8388
|
-
|
|
8468
|
+
const strictDecode = Schema.decodeUnknownEffect(schema, { onExcessProperty: "error" });
|
|
8469
|
+
const decoded = yield* Effect.result(strictDecode(input));
|
|
8470
|
+
if (Result.isSuccess(decoded)) return decoded.success;
|
|
8471
|
+
const repaired = unwrapDoubleEncoded(schema, input);
|
|
8472
|
+
if (repaired !== void 0) {
|
|
8473
|
+
const redecoded = yield* Effect.result(strictDecode(repaired));
|
|
8474
|
+
if (Result.isSuccess(redecoded)) {
|
|
8475
|
+
yield* Effect.logWarning("llm.structured repaired a double-encoded tool field before decoding");
|
|
8476
|
+
return redecoded.success;
|
|
8477
|
+
}
|
|
8478
|
+
}
|
|
8479
|
+
return yield* Effect.fail(LlmContractViolation.make({ reason: `tool payload does not satisfy its schema: ${String(decoded.failure)} (raw: ${preview(input)})` }));
|
|
8389
8480
|
});
|
|
8390
8481
|
|
|
8391
8482
|
//#endregion
|
|
@@ -8397,16 +8488,18 @@ const decodeToolInput = (schema, input) => input === void 0 ? Effect.fail(LlmCon
|
|
|
8397
8488
|
*/
|
|
8398
8489
|
const clampTokens = (requested) => Math.min(requested ?? 16384, MAX_TOKENS_CEILING);
|
|
8399
8490
|
/**
|
|
8400
|
-
* Build the request body. The `tool` argument selects the lane.
|
|
8401
|
-
* answers in prose. When it is present
|
|
8402
|
-
* `emit` call
|
|
8491
|
+
* Build the request body in the model's own dialect. The `tool` argument selects the lane.
|
|
8492
|
+
* When it is absent the model answers in prose. When it is present the request constrains
|
|
8493
|
+
* the model to exactly one schema-shaped answer: a forced `emit` tool call on the
|
|
8494
|
+
* Anthropic dialect, a strict `json_schema` response format on the OpenAI one.
|
|
8403
8495
|
*
|
|
8404
8496
|
* `system` is omitted rather than sent empty, because an empty system block is a distinct
|
|
8405
8497
|
* (and rejected) input from no system block at all. An omitted system also has nothing to cache, so
|
|
8406
8498
|
* `cacheSystem` over an absent or empty system emits no `system` key at all instead of an empty
|
|
8407
8499
|
* cached block.
|
|
8408
8500
|
*/
|
|
8409
|
-
const buildInvokeBody = (key, prompt, options, tool) =>
|
|
8501
|
+
const buildInvokeBody = (key, prompt, options, tool) => modelByKey(key).provider === "openai" ? buildOpenAiBody(prompt, options, tool) : buildAnthropicBody(key, prompt, options, tool);
|
|
8502
|
+
const buildAnthropicBody = (key, prompt, options, tool) => {
|
|
8410
8503
|
const body = {
|
|
8411
8504
|
anthropic_version: ANTHROPIC_VERSION,
|
|
8412
8505
|
max_tokens: clampTokens(options.maxTokens),
|
|
@@ -8437,6 +8530,51 @@ const buildInvokeBody = (key, prompt, options, tool) => {
|
|
|
8437
8530
|
return JSON.stringify(body);
|
|
8438
8531
|
};
|
|
8439
8532
|
/**
|
|
8533
|
+
* The OpenAI chat-completions body (probed live 2026-08-22 against
|
|
8534
|
+
* `global.openai.gpt-5.6-sol` — every field here is one the probe exercised).
|
|
8535
|
+
*
|
|
8536
|
+
* Differences from the Anthropic dialect, each one deliberate:
|
|
8537
|
+
*
|
|
8538
|
+
* - The token budget is `max_completion_tokens` and it bounds reasoning and answer
|
|
8539
|
+
* together, so the same clamp applies. 128k accepted at the ceiling.
|
|
8540
|
+
* - Effort is `reasoning_effort`, taking the same four values.
|
|
8541
|
+
* - `system` rides as a leading `{role: "system"}` message; there is no `system` field.
|
|
8542
|
+
* `cacheSystem` has no OpenAI-side marker — Bedrock reports `cache_write_tokens` in
|
|
8543
|
+
* this dialect's usage without an opt-in field — so the flag is accepted and unused
|
|
8544
|
+
* rather than rejected, keeping the option surface identical across lanes.
|
|
8545
|
+
* - The structured mechanism is `response_format.json_schema` with `strict: true`, named
|
|
8546
|
+
* `emit` so logs read the same across providers. `description` becomes the schema's
|
|
8547
|
+
* own `description`, the closest surface this dialect has to a tool description.
|
|
8548
|
+
*/
|
|
8549
|
+
const buildOpenAiBody = (prompt, options, tool) => {
|
|
8550
|
+
const messages = [];
|
|
8551
|
+
if (options.system !== void 0 && options.system.length > 0) messages.push({
|
|
8552
|
+
role: "system",
|
|
8553
|
+
content: options.system
|
|
8554
|
+
});
|
|
8555
|
+
messages.push({
|
|
8556
|
+
role: "user",
|
|
8557
|
+
content: prompt
|
|
8558
|
+
});
|
|
8559
|
+
const body = {
|
|
8560
|
+
max_completion_tokens: clampTokens(options.maxTokens),
|
|
8561
|
+
messages,
|
|
8562
|
+
reasoning_effort: options.effort
|
|
8563
|
+
};
|
|
8564
|
+
if (tool !== void 0) body.response_format = {
|
|
8565
|
+
type: "json_schema",
|
|
8566
|
+
json_schema: {
|
|
8567
|
+
name: STRUCTURED_TOOL_NAME,
|
|
8568
|
+
strict: true,
|
|
8569
|
+
schema: tool.description === void 0 ? tool.inputSchema : {
|
|
8570
|
+
description: tool.description,
|
|
8571
|
+
...tool.inputSchema
|
|
8572
|
+
}
|
|
8573
|
+
}
|
|
8574
|
+
};
|
|
8575
|
+
return JSON.stringify(body);
|
|
8576
|
+
};
|
|
8577
|
+
/**
|
|
8440
8578
|
* `stop_reason` values that mean the content is not a complete answer. Both become typed
|
|
8441
8579
|
* failures. A response cut off at `max_tokens` may never have reached the point that made
|
|
8442
8580
|
* it a judgment, and a refusal carries no judgment at all. Reading either as a finished
|
|
@@ -8445,6 +8583,55 @@ const buildInvokeBody = (key, prompt, options, tool) => {
|
|
|
8445
8583
|
const INCOMPLETE_STOP_REASONS = /* @__PURE__ */ new Set(["max_tokens", "refusal"]);
|
|
8446
8584
|
/** The parsed payload, read defensively, since every field on the wire is optional. */
|
|
8447
8585
|
const asResponseBody = (payload) => payload ?? {};
|
|
8586
|
+
/**
|
|
8587
|
+
* Fold an OpenAI chat-completions payload into {@link InvokeResponseBody}, so one read
|
|
8588
|
+
* side serves both dialects.
|
|
8589
|
+
*
|
|
8590
|
+
* `finish_reason` maps onto the Anthropic vocabulary this module already gates on:
|
|
8591
|
+
* `length` is `max_tokens` and `content_filter` is `refusal`, both of which
|
|
8592
|
+
* {@link INCOMPLETE_STOP_REASONS} already refuses; everything else passes through as a
|
|
8593
|
+
* complete answer. `structured` says how the caller asked, which decides how the content
|
|
8594
|
+
* is presented: a structured request's content is the schema-constrained JSON, parsed
|
|
8595
|
+
* here and presented as the `emit` tool's input, and a prose request's content is a text
|
|
8596
|
+
* block. Content that fails to parse on the structured path yields NO tool block, which
|
|
8597
|
+
* downstream reports as the existing "no tool_use block" violation — the right class,
|
|
8598
|
+
* since constrained decoding makes that a broken response rather than an off-schema one.
|
|
8599
|
+
*/
|
|
8600
|
+
const normalizeOpenAiResponse = (payload, structured) => {
|
|
8601
|
+
const body = payload ?? {};
|
|
8602
|
+
const choice = body.choices?.[0];
|
|
8603
|
+
const finish = choice?.finish_reason ?? null;
|
|
8604
|
+
const stopReason = finish === "length" ? "max_tokens" : finish === "content_filter" ? "refusal" : finish;
|
|
8605
|
+
const text = choice?.message?.content;
|
|
8606
|
+
const content = [];
|
|
8607
|
+
if (typeof text === "string" && text.length > 0) {
|
|
8608
|
+
if (structured) {
|
|
8609
|
+
const input = (() => {
|
|
8610
|
+
try {
|
|
8611
|
+
return { value: JSON.parse(text) };
|
|
8612
|
+
} catch {
|
|
8613
|
+
return;
|
|
8614
|
+
}
|
|
8615
|
+
})();
|
|
8616
|
+
if (input !== void 0) content.push({
|
|
8617
|
+
type: "tool_use",
|
|
8618
|
+
name: STRUCTURED_TOOL_NAME,
|
|
8619
|
+
input: input.value
|
|
8620
|
+
});
|
|
8621
|
+
} else content.push({
|
|
8622
|
+
type: "text",
|
|
8623
|
+
text
|
|
8624
|
+
});
|
|
8625
|
+
}
|
|
8626
|
+
return {
|
|
8627
|
+
stop_reason: stopReason,
|
|
8628
|
+
content,
|
|
8629
|
+
usage: {
|
|
8630
|
+
...body.usage?.prompt_tokens === void 0 ? {} : { input_tokens: body.usage.prompt_tokens },
|
|
8631
|
+
...body.usage?.completion_tokens === void 0 ? {} : { output_tokens: body.usage.completion_tokens }
|
|
8632
|
+
}
|
|
8633
|
+
};
|
|
8634
|
+
};
|
|
8448
8635
|
/** The incomplete `stop_reason`, or null when the response ran to a natural end. */
|
|
8449
8636
|
const incompleteReason = (parsed) => {
|
|
8450
8637
|
const stop = parsed.stop_reason ?? null;
|
|
@@ -8483,7 +8670,7 @@ const makeModelClient = (client) => {
|
|
|
8483
8670
|
description: tool.description
|
|
8484
8671
|
}));
|
|
8485
8672
|
const finished = yield* Effect.clockWith((clock) => clock.currentTimeMillis);
|
|
8486
|
-
const parsed = asResponseBody(payload);
|
|
8673
|
+
const parsed = model.provider === "openai" ? normalizeOpenAiResponse(payload, tool !== void 0) : asResponseBody(payload);
|
|
8487
8674
|
const incomplete = incompleteReason(parsed);
|
|
8488
8675
|
if (incomplete !== null) return yield* Effect.fail(ModelUnavailable.make({
|
|
8489
8676
|
modelId: model.modelId,
|
|
@@ -8834,17 +9021,42 @@ const keyMembers = (items, textOf, options) => {
|
|
|
8834
9021
|
};
|
|
8835
9022
|
};
|
|
8836
9023
|
/**
|
|
9024
|
+
* The offered key a model's answer denotes, or `undefined` when it denotes none.
|
|
9025
|
+
*
|
|
9026
|
+
* A model shown `<member_m3>` answers `member_m3` at least as readily as `m3`: the wrapper tag is
|
|
9027
|
+
* the only place most batch prompts DISPLAY a key, so the label-prefixed form is the one the prompt
|
|
9028
|
+
* itself teaches (measured live 2026-08-23: `gpt-5.6-sol` answers the prefixed form on every call,
|
|
9029
|
+
* Claude Sonnet 5 on most). So a key that does not match directly is retried once with everything
|
|
9030
|
+
* up to its last `_` stripped, and only a suffix the batch actually offered resolves — `member_m9`
|
|
9031
|
+
* in a batch of three still denotes nothing, and a path or an invented name still drops. The
|
|
9032
|
+
* canonical form is returned so two spellings of one member collapse to one key everywhere a phase
|
|
9033
|
+
* keeps per-key state.
|
|
9034
|
+
*/
|
|
9035
|
+
const offeredKeyFor = (batch, key) => {
|
|
9036
|
+
if (batch.itemForKey.has(key)) return key;
|
|
9037
|
+
const at = key.lastIndexOf("_");
|
|
9038
|
+
if (at === -1) return void 0;
|
|
9039
|
+
const suffix = key.slice(at + 1);
|
|
9040
|
+
return batch.itemForKey.has(suffix) ? suffix : void 0;
|
|
9041
|
+
};
|
|
9042
|
+
/**
|
|
8837
9043
|
* Resolve the keys a model named back to items: unknown keys are dropped, repeats collapse.
|
|
8838
9044
|
*
|
|
8839
9045
|
* A key the batch never offered is a member the model invented, and every phase on this kernel turns
|
|
8840
9046
|
* a named member into a write, so an unresolvable key must not reach that write. Dropping it leaves
|
|
8841
9047
|
* the corresponding file untouched, which is the safe outcome for every one of the five phases.
|
|
9048
|
+
* Resolution goes through {@link offeredKeyFor}, so the label-prefixed spelling of an offered key
|
|
9049
|
+
* (`member_m3` for `m3`) resolves rather than reading as an invention.
|
|
8842
9050
|
*
|
|
8843
9051
|
* The result keeps the order the model named the keys in, and a key named twice appears once.
|
|
8844
|
-
* De-duplication is on the
|
|
8845
|
-
*
|
|
8846
|
-
|
|
8847
|
-
|
|
9052
|
+
* De-duplication is on the CANONICAL key rather than on the spelling or the resolved item, so `m1`
|
|
9053
|
+
* and `member_m1` in one answer count as one member, and the count a phase gates on ("at least two
|
|
9054
|
+
* members absorbed") counts distinct offered members.
|
|
9055
|
+
*/
|
|
9056
|
+
const resolveKeys = (batch, keys) => [...new Set(keys.flatMap((key) => {
|
|
9057
|
+
const canonical = offeredKeyFor(batch, key);
|
|
9058
|
+
return canonical === void 0 ? [] : [canonical];
|
|
9059
|
+
}))].flatMap((key) => {
|
|
8848
9060
|
const item = batch.itemForKey.get(key);
|
|
8849
9061
|
return item === void 0 ? [] : [item];
|
|
8850
9062
|
});
|
|
@@ -9341,7 +9553,16 @@ const datePlusDays = (date, days) => {
|
|
|
9341
9553
|
//#endregion
|
|
9342
9554
|
//#region packages/sleep/dist/env.js
|
|
9343
9555
|
/**
|
|
9344
|
-
* Model assignments per LLM phase
|
|
9556
|
+
* Model assignments per LLM phase.
|
|
9557
|
+
*
|
|
9558
|
+
* Every structured phase names `gpt-5.6-sol`, and the reason is one wire property rather
|
|
9559
|
+
* than a model-quality judgment: its strict `json_schema` mode does constrained decoding
|
|
9560
|
+
* on Bedrock today, so an off-schema answer — including the double-encoded-string shape
|
|
9561
|
+
* that skipped 13 batches in one Claude-5 run (issue #53) — cannot be generated at all.
|
|
9562
|
+
* The Claude 5 models reject `strict` and `output_config.format` on every Bedrock surface
|
|
9563
|
+
* (probed live 2026-08-22), so with them the schema is a request the decode enforces
|
|
9564
|
+
* after the fact, and a violated batch is work lost. When Claude 5 structured outputs
|
|
9565
|
+
* land on Bedrock, re-deciding this map is a quality question again; today it is not.
|
|
9345
9566
|
*
|
|
9346
9567
|
* `trace-consolidation` names `opus-5` and does not thereby choose it. The consolidator is an eve
|
|
9347
9568
|
* agent that pins its own model in `apps/consolidator/agent/agent.ts`, and this map cannot reach that
|
|
@@ -9350,33 +9571,13 @@ const datePlusDays = (date, days) => {
|
|
|
9350
9571
|
* the Bedrock global endpoint, high reasoning effort, no cost ceiling.)
|
|
9351
9572
|
*/
|
|
9352
9573
|
const DEFAULT_MODELS = {
|
|
9353
|
-
|
|
9354
|
-
|
|
9355
|
-
|
|
9356
|
-
|
|
9357
|
-
|
|
9358
|
-
* code afterwards. The strong model is spent where prose gets written.
|
|
9359
|
-
*/
|
|
9360
|
-
"dedup-merge": "sonnet-5",
|
|
9361
|
-
/**
|
|
9362
|
-
* Sonnet, and one or two calls a night: the whole of one entity type's name list goes in one call.
|
|
9363
|
-
* The question is a partition over short strings with their evidence inline, not a synthesis, so
|
|
9364
|
-
* the strong model would buy nothing the deterministic floors around the answer do not already
|
|
9365
|
-
* supply.
|
|
9366
|
-
*/
|
|
9367
|
-
"entity-resolution": "sonnet-5",
|
|
9368
|
-
"edge-typing": "sonnet-5",
|
|
9369
|
-
"arc-synthesis": "opus-5",
|
|
9370
|
-
compress: "sonnet-5",
|
|
9574
|
+
"dedup-merge": "gpt-5.6-sol",
|
|
9575
|
+
"entity-resolution": "gpt-5.6-sol",
|
|
9576
|
+
"edge-typing": "gpt-5.6-sol",
|
|
9577
|
+
"arc-synthesis": "gpt-5.6-sol",
|
|
9578
|
+
compress: "gpt-5.6-sol",
|
|
9371
9579
|
"trace-consolidation": "opus-5",
|
|
9372
|
-
|
|
9373
|
-
* Sonnet, for the same reason `dedup-merge` and the edge-typing judge name it: the question is an
|
|
9374
|
-
* extraction over text the model is shown — which of these memories carries an open commitment,
|
|
9375
|
-
* quote the sentence — and every consequence is decided afterwards by code. The sentence has to be
|
|
9376
|
-
* VERBATIM, which is copying rather than composing, and the confidence floor plus the verbatim
|
|
9377
|
-
* check are what a stronger model would otherwise be buying.
|
|
9378
|
-
*/
|
|
9379
|
-
"task-detection": "sonnet-5"
|
|
9580
|
+
"task-detection": "gpt-5.6-sol"
|
|
9380
9581
|
};
|
|
9381
9582
|
/** The model a phase calls: the caller's override, else {@link DEFAULT_MODELS}, else sonnet. */
|
|
9382
9583
|
const modelFor = (deps, phase) => deps.models?.[phase] ?? DEFAULT_MODELS[phase] ?? "sonnet-5";
|
|
@@ -10862,7 +11063,9 @@ const compress = (env) => Effect.gen(function* () {
|
|
|
10862
11063
|
batches: batches.length,
|
|
10863
11064
|
canonicals: 0,
|
|
10864
11065
|
archived: 0,
|
|
10865
|
-
skipped: 0
|
|
11066
|
+
skipped: 0,
|
|
11067
|
+
failed: 0,
|
|
11068
|
+
refused: 0
|
|
10866
11069
|
};
|
|
10867
11070
|
if (batches.length === 0) return emptyOutcome(counts);
|
|
10868
11071
|
if (env.dryRun) return emptyOutcome(counts);
|
|
@@ -10870,7 +11073,16 @@ const compress = (env) => Effect.gen(function* () {
|
|
|
10870
11073
|
let llmCalls = 0;
|
|
10871
11074
|
let canonicals = 0;
|
|
10872
11075
|
let archived = 0;
|
|
11076
|
+
/**
|
|
11077
|
+
* `skipped` stays the total, and `failed` + `refused` partition it. The two are different
|
|
11078
|
+
* diagnoses with different fixes — a failed call is the model or the wire (already logged by
|
|
11079
|
+
* `isolate`), a refusal is an answer the phase declined to act on (logged below) — and a night
|
|
11080
|
+
* reporting only their sum cannot say which one it had. 47 of 47 batches once skipped as
|
|
11081
|
+
* refusals with nothing on stderr, and the sum read as flaky calls.
|
|
11082
|
+
*/
|
|
10873
11083
|
let skipped = 0;
|
|
11084
|
+
let failed = 0;
|
|
11085
|
+
let refused = 0;
|
|
10874
11086
|
let lastCommit = null;
|
|
10875
11087
|
for (const batch of batches) {
|
|
10876
11088
|
/** Opaque keys again, so `absorbedKeys` cannot name a path. */
|
|
@@ -10886,12 +11098,15 @@ const compress = (env) => Effect.gen(function* () {
|
|
|
10886
11098
|
});
|
|
10887
11099
|
if (synthesis === void 0) {
|
|
10888
11100
|
skipped += 1;
|
|
11101
|
+
failed += 1;
|
|
10889
11102
|
continue;
|
|
10890
11103
|
}
|
|
10891
11104
|
/** A key the batch never offered resolves to nothing, so a fold reaches only offered files. */
|
|
10892
11105
|
const absorbed = resolveKeys(keyed, synthesis.absorbedKeys).map((entry) => entry.row.path);
|
|
10893
11106
|
if (absorbed.length < 2 || synthesis.title.trim() === "" || synthesis.claim.trim() === "") {
|
|
10894
11107
|
skipped += 1;
|
|
11108
|
+
refused += 1;
|
|
11109
|
+
yield* Effect.logWarning(`sleep.llm compress batch of ${batch.length} refused: the model absorbed ${absorbed.length} of ${synthesis.absorbedKeys.length} named keys` + (synthesis.absorbedKeys.length > 0 && absorbed.length === 0 ? ` (none of the named keys resolved: ${synthesis.absorbedKeys.slice(0, 3).join(", ")}${synthesis.absorbedKeys.length > 3 ? ", …" : ""})` : ""));
|
|
10895
11110
|
continue;
|
|
10896
11111
|
}
|
|
10897
11112
|
/**
|
|
@@ -10904,6 +11119,8 @@ const compress = (env) => Effect.gen(function* () {
|
|
|
10904
11119
|
const members = excludeSelfSupersede(canonicalPath, absorbed);
|
|
10905
11120
|
if (members.length === 0) {
|
|
10906
11121
|
skipped += 1;
|
|
11122
|
+
refused += 1;
|
|
11123
|
+
yield* Effect.logWarning(`sleep.llm compress batch of ${batch.length} refused: every absorbed member was the canonical itself`);
|
|
10907
11124
|
continue;
|
|
10908
11125
|
}
|
|
10909
11126
|
/**
|
|
@@ -10918,6 +11135,8 @@ const compress = (env) => Effect.gen(function* () {
|
|
|
10918
11135
|
}
|
|
10919
11136
|
if (archivedPaths.length === 0) {
|
|
10920
11137
|
skipped += 1;
|
|
11138
|
+
refused += 1;
|
|
11139
|
+
yield* Effect.logWarning(`sleep.llm compress batch of ${batch.length} refused: every member was already gone from the tree`);
|
|
10921
11140
|
continue;
|
|
10922
11141
|
}
|
|
10923
11142
|
yield* writeFileBytes(env, canonicalPath, renderTemplate({
|
|
@@ -10936,7 +11155,9 @@ const compress = (env) => Effect.gen(function* () {
|
|
|
10936
11155
|
...counts,
|
|
10937
11156
|
canonicals,
|
|
10938
11157
|
archived,
|
|
10939
|
-
skipped
|
|
11158
|
+
skipped,
|
|
11159
|
+
failed,
|
|
11160
|
+
refused
|
|
10940
11161
|
});
|
|
10941
11162
|
if (commitSha !== null) lastCommit = commitSha;
|
|
10942
11163
|
}
|
|
@@ -10945,7 +11166,9 @@ const compress = (env) => Effect.gen(function* () {
|
|
|
10945
11166
|
...counts,
|
|
10946
11167
|
canonicals,
|
|
10947
11168
|
archived,
|
|
10948
|
-
skipped
|
|
11169
|
+
skipped,
|
|
11170
|
+
failed,
|
|
11171
|
+
refused
|
|
10949
11172
|
},
|
|
10950
11173
|
commitSha: lastCommit,
|
|
10951
11174
|
llmCalls
|
|
@@ -11856,7 +12079,9 @@ const dedupMerge = (env) => Effect.gen(function* () {
|
|
|
11856
12079
|
candidates: oriented.length,
|
|
11857
12080
|
components: 0,
|
|
11858
12081
|
llmGroups: 0,
|
|
11859
|
-
vetoed: oriented.length - decisions.length
|
|
12082
|
+
vetoed: oriented.length - decisions.length,
|
|
12083
|
+
skipped: 0,
|
|
12084
|
+
unresolved: 0
|
|
11860
12085
|
},
|
|
11861
12086
|
/**
|
|
11862
12087
|
* Every mined pair on this arm cleared 0.92, so a vetoed one here is a near-certain duplicate
|
|
@@ -11927,6 +12152,12 @@ const dedupMerge = (env) => Effect.gen(function* () {
|
|
|
11927
12152
|
let llmCalls = 0;
|
|
11928
12153
|
let llmGroups = 0;
|
|
11929
12154
|
let skipped = 0;
|
|
12155
|
+
/**
|
|
12156
|
+
* Model-named member keys that resolved to no offered member, dropped unacted. The drop is the
|
|
12157
|
+
* safe outcome and stays; the count makes a systematic naming pattern visible instead of reading
|
|
12158
|
+
* as a night in which the model proposed no merges (issue #58).
|
|
12159
|
+
*/
|
|
12160
|
+
let unresolved = 0;
|
|
11930
12161
|
/** Group-implied pairs, in batch then component then group order. */
|
|
11931
12162
|
const groupPairs = [];
|
|
11932
12163
|
/** Every path a surviving group claimed, so the mined arm cannot re-propose one. */
|
|
@@ -11966,11 +12197,17 @@ const dedupMerge = (env) => Effect.gen(function* () {
|
|
|
11966
12197
|
skipped += 1;
|
|
11967
12198
|
continue;
|
|
11968
12199
|
}
|
|
12200
|
+
/** This batch's unresolvable keys, so the warning below can name the spellings that failed. */
|
|
12201
|
+
const unresolvedKeys = [];
|
|
11969
12202
|
for (const group of partition.groups) {
|
|
12203
|
+
const dropped = group.memberKeys.filter((key) => offeredKeyFor(keyed, key) === void 0);
|
|
12204
|
+
unresolved += dropped.length;
|
|
12205
|
+
unresolvedKeys.push(...dropped);
|
|
11970
12206
|
const members = resolveKeys(keyed, group.memberKeys);
|
|
11971
12207
|
if (members.length < 2) continue;
|
|
11972
12208
|
if (new Set(group.memberKeys.flatMap((key) => {
|
|
11973
|
-
const
|
|
12209
|
+
const canonical = offeredKeyFor(keyed, key);
|
|
12210
|
+
const id = canonical === void 0 ? void 0 : componentOfKey.get(canonical);
|
|
11974
12211
|
return id === void 0 ? [] : [id];
|
|
11975
12212
|
})).size !== 1) continue;
|
|
11976
12213
|
/** The keeper is the OLDEST member: the lowest corpus offset, the same rule a pair uses. */
|
|
@@ -11996,6 +12233,7 @@ const dedupMerge = (env) => Effect.gen(function* () {
|
|
|
11996
12233
|
}
|
|
11997
12234
|
grouped.add(keeper.path);
|
|
11998
12235
|
}
|
|
12236
|
+
if (unresolvedKeys.length > 0) yield* Effect.logWarning(`sleep.llm dedup batch of ${batch.length} components dropped ${unresolvedKeys.length} member keys naming no offered member (${unresolvedKeys.slice(0, 3).join(", ")}${unresolvedKeys.length > 3 ? ", …" : ""})`);
|
|
11999
12237
|
}
|
|
12000
12238
|
/**
|
|
12001
12239
|
* Groups first, then the mined pairs above the DETERMINISTIC floor that no group claimed. The
|
|
@@ -12015,7 +12253,8 @@ const dedupMerge = (env) => Effect.gen(function* () {
|
|
|
12015
12253
|
components: components.length,
|
|
12016
12254
|
llmGroups,
|
|
12017
12255
|
vetoed: proposed.length - decisions.length,
|
|
12018
|
-
skipped
|
|
12256
|
+
skipped,
|
|
12257
|
+
unresolved
|
|
12019
12258
|
},
|
|
12020
12259
|
/**
|
|
12021
12260
|
* On this arm a vetoed pair is one the MODEL grouped as the same memory, or one that cleared
|
|
@@ -12026,11 +12265,13 @@ const dedupMerge = (env) => Effect.gen(function* () {
|
|
|
12026
12265
|
*
|
|
12027
12266
|
* `judged` is false when a batch's call failed, because those components were never partitioned:
|
|
12028
12267
|
* their pairs reach the veto only through the mined arm, so a night that lost a call cannot say
|
|
12029
|
-
* whether a pair it did not see is still a candidate.
|
|
12268
|
+
* whether a pair it did not see is still a candidate. An unresolved member key is the same
|
|
12269
|
+
* hazard from the answer side — a group the phase could not fully map was not fully judged —
|
|
12270
|
+
* so it holds the sweep back the same way.
|
|
12030
12271
|
*/
|
|
12031
12272
|
{
|
|
12032
12273
|
vetoed: vetoedPairs(proposed),
|
|
12033
|
-
judged: skipped === 0
|
|
12274
|
+
judged: skipped === 0 && unresolved === 0
|
|
12034
12275
|
}
|
|
12035
12276
|
),
|
|
12036
12277
|
llmCalls
|
|
@@ -12455,6 +12696,7 @@ const edgeTyping = (env) => Effect.gen(function* () {
|
|
|
12455
12696
|
contradictions: 0,
|
|
12456
12697
|
promoted: 0,
|
|
12457
12698
|
skipped: 0,
|
|
12699
|
+
unresolved: 0,
|
|
12458
12700
|
capped: 0,
|
|
12459
12701
|
duplicates: 0,
|
|
12460
12702
|
tasksMinted: 0,
|
|
@@ -12521,6 +12763,13 @@ const edgeTyping = (env) => Effect.gen(function* () {
|
|
|
12521
12763
|
let capped = 0;
|
|
12522
12764
|
/** Second-and-later verdicts naming a key their batch had already answered for. */
|
|
12523
12765
|
let duplicates = 0;
|
|
12766
|
+
/**
|
|
12767
|
+
* Verdicts whose key resolved to no offered pair, so they were dropped unacted. Dropping is the
|
|
12768
|
+
* safe outcome and stays; the count is what makes a SYSTEMATIC pattern visible — a model naming
|
|
12769
|
+
* keys in a spelling the resolver refuses drops every verdict of every batch, and a night that
|
|
12770
|
+
* judged nothing looked identical to a night whose model answered nothing (issue #58).
|
|
12771
|
+
*/
|
|
12772
|
+
let unresolved = 0;
|
|
12524
12773
|
let llmCalls = 0;
|
|
12525
12774
|
/**
|
|
12526
12775
|
* Contradictions this night detected for the FIRST time, so below the promotion gate.
|
|
@@ -12561,18 +12810,27 @@ const edgeTyping = (env) => Effect.gen(function* () {
|
|
|
12561
12810
|
* rather than silently swallowed, so a model doing this is visible in a night's report.
|
|
12562
12811
|
*/
|
|
12563
12812
|
const answered = /* @__PURE__ */ new Set();
|
|
12813
|
+
/** This batch's dropped verdicts, so the warning below can name the spellings that failed. */
|
|
12814
|
+
const unresolvedKeys = [];
|
|
12564
12815
|
for (const verdict of answer.verdicts) {
|
|
12565
12816
|
/**
|
|
12566
12817
|
* The key is resolved through the kernel, so an invented key yields no candidate and no
|
|
12567
|
-
* write.
|
|
12818
|
+
* write. The CANONICAL key feeds the repeat guard, so `m1` and its label-prefixed spelling
|
|
12819
|
+
* `pair_m1` in one answer are one pair answered twice, not two pairs.
|
|
12568
12820
|
*/
|
|
12569
|
-
const
|
|
12821
|
+
const pairKey = offeredKeyFor(keyed, verdict.pairKey);
|
|
12822
|
+
if (pairKey === void 0) {
|
|
12823
|
+
unresolved += 1;
|
|
12824
|
+
unresolvedKeys.push(verdict.pairKey);
|
|
12825
|
+
continue;
|
|
12826
|
+
}
|
|
12827
|
+
const [candidate] = resolveKeys(keyed, [pairKey]);
|
|
12570
12828
|
if (candidate === void 0) continue;
|
|
12571
|
-
if (answered.has(
|
|
12829
|
+
if (answered.has(pairKey)) {
|
|
12572
12830
|
duplicates += 1;
|
|
12573
12831
|
continue;
|
|
12574
12832
|
}
|
|
12575
|
-
answered.add(
|
|
12833
|
+
answered.add(pairKey);
|
|
12576
12834
|
judged += 1;
|
|
12577
12835
|
if (!assertsEdge(verdict)) continue;
|
|
12578
12836
|
if (assertsContradiction(verdict)) {
|
|
@@ -12675,14 +12933,18 @@ const edgeTyping = (env) => Effect.gen(function* () {
|
|
|
12675
12933
|
const [subject, object] = verdict.direction === "src_to_dst" ? [candidate.pair.src, candidate.pair.dst] : [candidate.pair.dst, candidate.pair.src];
|
|
12676
12934
|
if (yield* stampFile(env, subject, [link(verdict.rel, hrefFor(object)), meta("memhtml-updated", env.at)])) typed += 1;
|
|
12677
12935
|
}
|
|
12936
|
+
if (unresolvedKeys.length > 0) yield* Effect.logWarning(`sleep.llm edge-typing batch of ${batch.length} dropped ${unresolvedKeys.length} verdicts naming no offered pair (${unresolvedKeys.slice(0, 3).join(", ")}${unresolvedKeys.length > 3 ? ", …" : ""})`);
|
|
12678
12937
|
}
|
|
12679
12938
|
/**
|
|
12680
12939
|
* The single-detection contradictions become tasks in the SAME commit as the promotions. The
|
|
12681
12940
|
* sweep is gated on a night that judged its whole candidate set: `skipped` counts pairs whose
|
|
12682
12941
|
* batch's call failed as well as pairs whose endpoint the tree no longer holds, and a pair the
|
|
12683
12942
|
* model was never asked about must not read as a pair the model stopped contradicting.
|
|
12943
|
+
* `unresolved` is the same hazard from the answer side — a verdict the phase could not map to a
|
|
12944
|
+
* pair is a pair that was never judged, so its held-back contradiction must not close as "no
|
|
12945
|
+
* longer detected" over a misspelled key.
|
|
12684
12946
|
*/
|
|
12685
|
-
const tasks = yield* mintContradictionTasks(env, deferred, skipped === 0);
|
|
12947
|
+
const tasks = yield* mintContradictionTasks(env, deferred, skipped === 0 && unresolved === 0);
|
|
12686
12948
|
const counts = {
|
|
12687
12949
|
candidates: candidates.length,
|
|
12688
12950
|
judged,
|
|
@@ -12690,6 +12952,7 @@ const edgeTyping = (env) => Effect.gen(function* () {
|
|
|
12690
12952
|
contradictions,
|
|
12691
12953
|
promoted,
|
|
12692
12954
|
skipped,
|
|
12955
|
+
unresolved,
|
|
12693
12956
|
capped,
|
|
12694
12957
|
duplicates,
|
|
12695
12958
|
tasksMinted: tasks.minted,
|
|
@@ -13206,6 +13469,12 @@ const entityResolution = (env) => Effect.gen(function* () {
|
|
|
13206
13469
|
/** Model calls that came back malformed. The sweep's precondition reads this; see below. */
|
|
13207
13470
|
let callsFailed = 0;
|
|
13208
13471
|
/**
|
|
13472
|
+
* Cluster member keys (or canonical keys) resolving to no offered member, dropped unacted. The
|
|
13473
|
+
* drop is the safe outcome and stays; the count makes a systematic naming pattern visible
|
|
13474
|
+
* instead of reading as a night in which the model proposed no merges (issue #58).
|
|
13475
|
+
*/
|
|
13476
|
+
let unresolved = 0;
|
|
13477
|
+
/**
|
|
13209
13478
|
* Every pair this night deferred to a human, as a value rather than only a count.
|
|
13210
13479
|
*
|
|
13211
13480
|
* This is issue #44's motivating case in one variable. The phase used to report
|
|
@@ -13317,6 +13586,8 @@ const entityResolution = (env) => Effect.gen(function* () {
|
|
|
13317
13586
|
callsFailed += 1;
|
|
13318
13587
|
continue;
|
|
13319
13588
|
}
|
|
13589
|
+
/** This shard's unresolvable keys, so the warning below names the spellings that failed. */
|
|
13590
|
+
const unresolvedKeys = [];
|
|
13320
13591
|
for (const cluster of clustering.clusters) {
|
|
13321
13592
|
/**
|
|
13322
13593
|
* A key the batch never offered resolves to nothing, so an invented member cannot become a
|
|
@@ -13324,6 +13595,9 @@ const entityResolution = (env) => Effect.gen(function* () {
|
|
|
13324
13595
|
* canonical is outside it contradicts itself, and guessing which half was meant would be
|
|
13325
13596
|
* the caller inventing a merge.
|
|
13326
13597
|
*/
|
|
13598
|
+
const droppedKeys = [...cluster.memberKeys, cluster.canonicalKey].filter((key) => offeredKeyFor(keyed, key) === void 0);
|
|
13599
|
+
unresolved += droppedKeys.length;
|
|
13600
|
+
unresolvedKeys.push(...droppedKeys);
|
|
13327
13601
|
const memberNames = resolveKeys(keyed, cluster.memberKeys).map((centroid) => centroid.name);
|
|
13328
13602
|
const [canonicalMember] = resolveKeys(keyed, [cluster.canonicalKey]);
|
|
13329
13603
|
if (canonicalMember === void 0 || !memberNames.includes(canonicalMember.name)) continue;
|
|
@@ -13382,6 +13656,7 @@ const entityResolution = (env) => Effect.gen(function* () {
|
|
|
13382
13656
|
});
|
|
13383
13657
|
}
|
|
13384
13658
|
}
|
|
13659
|
+
if (unresolvedKeys.length > 0) yield* Effect.logWarning(`sleep.llm entity-resolution ${entityType} batch of ${shard.length} dropped ${unresolvedKeys.length} cluster keys naming no offered member (${unresolvedKeys.slice(0, 3).join(", ")}${unresolvedKeys.length > 3 ? ", …" : ""})`);
|
|
13385
13660
|
}
|
|
13386
13661
|
}
|
|
13387
13662
|
/**
|
|
@@ -13419,6 +13694,8 @@ const entityResolution = (env) => Effect.gen(function* () {
|
|
|
13419
13694
|
aliasMerges,
|
|
13420
13695
|
pendingCorroboration,
|
|
13421
13696
|
reviewCandidates,
|
|
13697
|
+
callsFailed,
|
|
13698
|
+
unresolved,
|
|
13422
13699
|
tasksMinted: 0,
|
|
13423
13700
|
tasksFramed: 0,
|
|
13424
13701
|
tasksDismissed: 0,
|
|
@@ -13444,7 +13721,12 @@ const entityResolution = (env) => Effect.gen(function* () {
|
|
|
13444
13721
|
* 1. The old early return on `rewrites.size === 0` would have skipped exactly the night this
|
|
13445
13722
|
* feature exists for: a night whose only outcome was deferrals is a night with no rewrites.
|
|
13446
13723
|
*/
|
|
13447
|
-
|
|
13724
|
+
/**
|
|
13725
|
+
* `unresolved === 0` joins the sweep gate for the reason `callsFailed === 0` is already in it: a
|
|
13726
|
+
* cluster key the phase could not map to a member is a name that was never judged, and sweeping
|
|
13727
|
+
* against a night that lost part of an answer closes reviews over a misspelling.
|
|
13728
|
+
*/
|
|
13729
|
+
const tasks = yield* mintReviewTasks(env, deferred, model !== void 0 && callsFailed === 0 && unresolved === 0);
|
|
13448
13730
|
let rewritten = 0;
|
|
13449
13731
|
for (const [path, pairs] of [...rewrites.entries()].sort(([left], [right]) => left < right ? -1 : 1)) {
|
|
13450
13732
|
const html = yield* readFileBytes(env, path);
|
|
@@ -14441,6 +14723,12 @@ const taskDetection = (env) => Effect.gen(function* () {
|
|
|
14441
14723
|
let dismissed = 0;
|
|
14442
14724
|
let skipped = 0;
|
|
14443
14725
|
/**
|
|
14726
|
+
* Findings whose key resolved to no offered member, dropped unacted. The drop is the safe
|
|
14727
|
+
* outcome and stays; the count makes a systematic pattern visible, because a night whose every
|
|
14728
|
+
* finding named an unresolvable key otherwise reads as a night with no open work (issue #58).
|
|
14729
|
+
*/
|
|
14730
|
+
let unresolved = 0;
|
|
14731
|
+
/**
|
|
14444
14732
|
* Every key this night's scan SAW, whether or not it minted and whether or not it cleared the floor.
|
|
14445
14733
|
* The sweep's input; see the `liveKeys.add` below for why the floor is not a filter here.
|
|
14446
14734
|
*/
|
|
@@ -14468,11 +14756,20 @@ const taskDetection = (env) => Effect.gen(function* () {
|
|
|
14468
14756
|
* because it is called one key at a time here — a finding names one member.
|
|
14469
14757
|
*/
|
|
14470
14758
|
const answered = /* @__PURE__ */ new Set();
|
|
14759
|
+
/** This batch's dropped findings, so the warning below can name the spellings that failed. */
|
|
14760
|
+
const unresolvedKeys = [];
|
|
14471
14761
|
for (const finding of answer.findings) {
|
|
14472
|
-
|
|
14762
|
+
/** The CANONICAL key feeds the repeat guard, same as edge-typing's, and for the same reason. */
|
|
14763
|
+
const memberKey = offeredKeyFor(keyed, finding.memberKey);
|
|
14764
|
+
if (memberKey === void 0) {
|
|
14765
|
+
unresolved += 1;
|
|
14766
|
+
unresolvedKeys.push(finding.memberKey);
|
|
14767
|
+
continue;
|
|
14768
|
+
}
|
|
14769
|
+
const [row] = resolveKeys(keyed, [memberKey]);
|
|
14473
14770
|
if (row === void 0) continue;
|
|
14474
|
-
if (answered.has(
|
|
14475
|
-
answered.add(
|
|
14771
|
+
if (answered.has(memberKey)) continue;
|
|
14772
|
+
answered.add(memberKey);
|
|
14476
14773
|
findings += 1;
|
|
14477
14774
|
/**
|
|
14478
14775
|
* The key is the SOURCE PATH plus the normalized sentence, so the same commitment found again
|
|
@@ -14518,13 +14815,17 @@ const taskDetection = (env) => Effect.gen(function* () {
|
|
|
14518
14815
|
else if (outcome === "framed") framed += 1;
|
|
14519
14816
|
else if (outcome === "dismissed") dismissed += 1;
|
|
14520
14817
|
}
|
|
14818
|
+
if (unresolvedKeys.length > 0) yield* Effect.logWarning(`sleep.llm task-detection batch of ${batch.length} dropped ${unresolvedKeys.length} findings naming no offered member (${unresolvedKeys.slice(0, 3).join(", ")}${unresolvedKeys.length > 3 ? ", …" : ""})`);
|
|
14521
14819
|
}
|
|
14522
14820
|
/**
|
|
14523
14821
|
* The sweep, only from a full-strength scan. `skipped > 0` means at least one batch's memories
|
|
14524
14822
|
* went unread, so a finding of theirs is missing from `liveKeys` because the phase could not look
|
|
14525
|
-
* rather than because it is gone.
|
|
14823
|
+
* rather than because it is gone. `unresolved > 0` is the same hazard from the answer side: the
|
|
14824
|
+
* model reported a finding the phase could not map to a member, so its detection key was never
|
|
14825
|
+
* constructed, and sweeping against that would close a live task because the model misspelled a
|
|
14826
|
+
* key rather than because the finding vanished.
|
|
14526
14827
|
*/
|
|
14527
|
-
const closed = skipped === 0 ? yield* closeVanishedDetections(env, TASK_DETECT_DETECTOR, liveKeys) : 0;
|
|
14828
|
+
const closed = skipped === 0 && unresolved === 0 ? yield* closeVanishedDetections(env, TASK_DETECT_DETECTOR, liveKeys) : 0;
|
|
14528
14829
|
const counts = {
|
|
14529
14830
|
candidates: candidates.length,
|
|
14530
14831
|
batches: batches.length,
|
|
@@ -14536,7 +14837,8 @@ const taskDetection = (env) => Effect.gen(function* () {
|
|
|
14536
14837
|
dismissed,
|
|
14537
14838
|
closed,
|
|
14538
14839
|
capped: budget.overflow,
|
|
14539
|
-
skipped
|
|
14840
|
+
skipped,
|
|
14841
|
+
unresolved
|
|
14540
14842
|
};
|
|
14541
14843
|
/**
|
|
14542
14844
|
* A refresh writes a `memhtml-updated` stamp, which is a staged file, so it commits — the queue's
|
|
@@ -14572,7 +14874,8 @@ const ZERO = {
|
|
|
14572
14874
|
dismissed: 0,
|
|
14573
14875
|
closed: 0,
|
|
14574
14876
|
capped: 0,
|
|
14575
|
-
skipped: 0
|
|
14877
|
+
skipped: 0,
|
|
14878
|
+
unresolved: 0
|
|
14576
14879
|
};
|
|
14577
14880
|
|
|
14578
14881
|
//#endregion
|
|
@@ -16685,4 +16988,4 @@ const latest = (left, right) => left === null ? right : right === null ? left :
|
|
|
16685
16988
|
|
|
16686
16989
|
//#endregion
|
|
16687
16990
|
export { STATE_DB_PATH as $, IndexRecorder as A, IndexGit as B, ModelClient as C, EmbeddingsLive as D, Embeddings as E, makeRetrieval as F, MIGRATIONS_DIR as G, sanitizeFtsQuery as H, reinforce as I, Store as J, STATE_MIGRATIONS_DIR as K, Indexer as L, persistScanned as M, readWatermark as N, EMBED_DIM as O, Retrieval as P, SLEEP_REPORTS_DIR as Q, makeIndexer as R, runDiscrimination as S, wrapAsData as T, DatabaseService as U, makeGitPort as V, makeDatabase as W, makeStore as X, expandRoot as Y, INDEX_DB_PATH as Z, meta as _, parseSidecar as a, makeGit as at, isSleepPhase as b, generateArtifacts as c, setMeta as ct, allPaths as d, fenceOpeningOf as dt, STATE_SIDECAR_PATH as et, danglingEdges as f, REINFORCE_SIGNALS as ft, link as g, hrefFor as h, makeSleep as i, Git as it, makeIndexRecorder as j, EMBED_WATERMARK as k, DETECTION_PREFIX as l, isValidDatetime as lt, applyHeadEdits as m, scanTraceRoot as n, initRepo as nt, renderSidecar as o, commitSubject as ot, publishRows as p, frameKeyOf as pt, STATE_SCHEMA as q, Sleep as r, readFileOrNull as rt, archivedFormOf as s, checkMemory as st, mergeTailExtract as t, attemptIo as tt, accessRows as u, closesFence as ut, unlink as v, ModelClientLive as w, discriminationGate as x, SLEEP_PHASES as y, readIndexState as z };
|
|
16688
|
-
//# sourceMappingURL=dist-
|
|
16991
|
+
//# sourceMappingURL=dist-DgKlozi6.mjs.map
|