@orkestrel/agent 0.0.36 → 0.0.38

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.
@@ -2756,6 +2756,7 @@ var AgentJudge = class {
2756
2756
  *
2757
2757
  * @remarks
2758
2758
  * The caller supplies the server origin and model. Every question travels in one request.
2759
+ * Wire keys are zero-based positions in question entry order; answers return under the caller's ids.
2759
2760
  * Server measures are ignored; the response model is preserved.
2760
2761
  * `headers` supplies authentication through the shared judge engine.
2761
2762
  * The example readings are those of the response Ollama 0.40.0 returned for that request on 2026-10-07.
@@ -2812,7 +2813,7 @@ var SystemOneJudge = class extends AgentJudge {
2812
2813
  /** Identifies the System One protocol. */
2813
2814
  name = "systemone";
2814
2815
  /**
2815
- * Projects the state and questions onto the System One request with the configured model.
2816
+ * Projects the state and questions onto the System One request with the configured model and positional keys.
2816
2817
  *
2817
2818
  * @param request - The state and questions keyed by caller id
2818
2819
  * @returns The System One request body
@@ -2821,11 +2822,11 @@ var SystemOneJudge = class extends AgentJudge {
2821
2822
  return {
2822
2823
  state: request.state,
2823
2824
  model: this.model,
2824
- questions: Object.fromEntries(Object.entries(request.questions).map(([id, question]) => [id, questionToSystemOne(question)]))
2825
+ questions: Object.fromEntries(Object.values(request.questions).map((question, index) => [String(index), questionToSystemOne(question)]))
2825
2826
  };
2826
2827
  }
2827
2828
  /**
2828
- * Decodes requested System One answers and reports the server model and available usage.
2829
+ * Decodes positional System One answers under caller ids and reports the server model and available usage.
2829
2830
  *
2830
2831
  * @param value - The parsed response body
2831
2832
  * @param request - The questions defining the expected answers
@@ -2835,9 +2836,10 @@ var SystemOneJudge = class extends AgentJudge {
2835
2836
  */
2836
2837
  read(value, request) {
2837
2838
  if (!isSystemOneResponse(value)) throw new JudgeError("PROTOCOL", `judge error: invalid System One response envelope; received ${(0, _orkestrel_contract.preview)(value)}`);
2838
- const entries = Object.entries(request.questions).map(([id, question]) => {
2839
- if (!Object.hasOwn(value.answers, id)) throw new JudgeError("PROTOCOL", `judge error: question ${id} has no System One answer`);
2840
- const wire = value.answers[id];
2839
+ const entries = Object.entries(request.questions).map(([id, question], index) => {
2840
+ const position = String(index);
2841
+ if (!Object.hasOwn(value.answers, position)) throw new JudgeError("PROTOCOL", `judge error: question ${id} has no System One answer`);
2842
+ const wire = value.answers[position];
2841
2843
  if (!isSystemOneAnswer(wire)) throw new JudgeError("PROTOCOL", `judge error: question ${id} has an invalid System One answer; received ${(0, _orkestrel_contract.preview)(wire)}`);
2842
2844
  const answer = extractSystemOneAnswer(wire, question);
2843
2845
  if (answer === void 0) throw new JudgeError("PROTOCOL", `judge error: question ${id} has a mismatched or incomplete System One answer`);
@@ -8806,6 +8808,11 @@ var PLACED_CATEGORIES = Object.freeze(["rule", "correction"]);
8806
8808
  var LEDGER_RULES_KEY = "rules";
8807
8809
  /** Prefixes the key of an owner's record, which the owner's id follows. */
8808
8810
  var LEDGER_OWNER_PREFIX = "owner:";
8811
+ /** Labels recalled conversation text by its origin. */
8812
+ var LEDGER_RECALL_LABELS = Object.freeze({
8813
+ request: "Earlier request:",
8814
+ reply: "Earlier reply:"
8815
+ });
8809
8816
  /**
8810
8817
  * Supplies the wording of every question the ledger asks its judge.
8811
8818
  *
@@ -8953,6 +8960,108 @@ function isLedgerError(value) {
8953
8960
  }
8954
8961
  // src/core/ledgers/helpers.ts
8955
8962
  /**
8963
+ * Digests held messages, lookup call context, stored copies, and sentence receipts using canonical text.
8964
+ * This non-cryptographic checksum detects host edits and makes no security claim.
8965
+ * @param exchanges - The stored exchange entries
8966
+ * @param messages - The conversation holding their messages
8967
+ * @returns The hexadecimal checksum, or undefined when a required message is absent or canonical encoding fails
8968
+ * @example
8969
+ * ```ts
8970
+ * digestLedgerWindow([], []) // a deterministic hexadecimal checksum
8971
+ * ```
8972
+ */
8973
+ function digestLedgerWindow(exchanges, messages) {
8974
+ const byId = new Map(messages.map((message) => [message.id, message]));
8975
+ const held = [...new Set(exchanges.flatMap((entry) => [
8976
+ entry.request,
8977
+ entry.reply,
8978
+ ...entry.lines.map((line) => line.source)
8979
+ ]))].map((id) => byId.get(id));
8980
+ const callers = messages.filter((message) => message.role === "tool" || message.calls !== void 0);
8981
+ const encoded = (0, _orkestrel_contract.attempt)(() => {
8982
+ const values = [...held, ...callers].map((message) => (0, _orkestrel_contract.canonicalStringify)(message));
8983
+ return values.some((value) => value === void 0) ? void 0 : (0, _orkestrel_contract.canonicalStringify)([values, exchanges.map((entry) => [entry.copy ?? null, entry.lines])]);
8984
+ });
8985
+ if (!encoded.success || encoded.value === void 0) return void 0;
8986
+ const hash = [...encoded.value].reduce((prior, character) => {
8987
+ const code = character.codePointAt(0) ?? 0;
8988
+ return {
8989
+ first: Math.imul(prior.first ^ code, 16777619) >>> 0,
8990
+ second: Math.imul(prior.second ^ code, 16777619) >>> 0
8991
+ };
8992
+ }, {
8993
+ first: 2166136261,
8994
+ second: 3335557771
8995
+ });
8996
+ return hash.first.toString(16).padStart(8, "0") + hash.second.toString(16).padStart(8, "0");
8997
+ }
8998
+ /**
8999
+ * Collects stored messages eligible to open the recall offer.
9000
+ * @remarks
9001
+ * Notes and findings without a result do not open the offer. User statements, including quiet
9002
+ * statements available through recall's word route, can open it; this helper does not filter results.
9003
+ * @param input - The messages, findings, and classification
9004
+ * @param requests - The marked request ids
9005
+ * @param notes - The annotation ids to exclude
9006
+ * @returns User statements, lookup findings with results, marked requests, and nonblank call-free replies
9007
+ * @example
9008
+ * ```ts
9009
+ * collectRecallMessages(input, new Set(), new Set()) // eligible stored messages
9010
+ * ```
9011
+ */
9012
+ function collectRecallMessages(input, requests, notes) {
9013
+ const findings = new Set(input.findings.filter((finding) => finding.result !== void 0).map((finding) => finding.id));
9014
+ return input.messages.filter((message) => !notes.has(message.id) && (requests.has(message.id) || message.role === "assistant" && !hasCalls(message) && message.content.trim() !== "" || message.role === "user" && !hasCalls(message) || message.role === "tool" && findings.has(message.id)));
9015
+ }
9016
+ /**
9017
+ * Checks whether an eligible earlier message is absent from the prompt or incompletely covered.
9018
+ * A successful finding is held when a finding with the same canonical identity and text is fully received.
9019
+ * @param input - The messages, findings, and classification
9020
+ * @param request - The current request id; only messages before it count
9021
+ * @param requests - The marked request ids
9022
+ * @param notes - The annotation ids to exclude
9023
+ * @param messages - The seed tail and held exchange messages; lookup stubs retain their rendered text
9024
+ * @param lines - The receipts held by the window and the request copy together
9025
+ * @returns True if an eligible earlier message is absent or only partly covered; false otherwise
9026
+ * @example
9027
+ * ```ts
9028
+ * hasHiddenRecall(input, request.id, requests, notes, tail, lines) // whether to offer recall
9029
+ * ```
9030
+ */
9031
+ function hasHiddenRecall(input, request, requests, notes, messages, lines) {
9032
+ const start = input.messages.findIndex((message) => message.id === request);
9033
+ const before = new Set(input.messages.slice(0, Math.max(0, start)).map((message) => message.id));
9034
+ const present = new Map(messages.map((message) => [message.id, message]));
9035
+ const receipts = Map.groupBy(lines, (line) => line.source);
9036
+ return collectRecallMessages(input, requests, notes).some((message) => {
9037
+ if (!before.has(message.id)) return false;
9038
+ if (requests.has(message.id) || message.role === "assistant") return !present.has(message.id);
9039
+ if (present.get(message.id)?.content === message.content) return false;
9040
+ const finding = input.findings.find((one) => one.id === message.id);
9041
+ return !(finding === void 0 ? [message.id] : input.findings.filter((one) => before.has(one.id) && matchesLookup(finding, one)).map((one) => one.id)).some((id) => {
9042
+ if (present.get(id)?.content === message.content) return true;
9043
+ const held = new Set((receipts.get(id) ?? []).map((line) => line.sentence));
9044
+ return splitSentences(message.content).every((_, at) => held.has(at));
9045
+ });
9046
+ });
9047
+ }
9048
+ /**
9049
+ * Checks whether two successful findings carry the same text for the same canonical lookup.
9050
+ * @param left - The finding to compare
9051
+ * @param right - The candidate copy
9052
+ * @returns True for the same finding or equal canonical identities and text; unkeyable findings match only themselves
9053
+ * @example
9054
+ * ```ts
9055
+ * matchesLookup(finding, finding) // true when it has a result
9056
+ * ```
9057
+ */
9058
+ function matchesLookup(left, right) {
9059
+ if (left.result === void 0 || right.result === void 0 || left.text !== right.text) return false;
9060
+ if (left.id === right.id) return true;
9061
+ const identity = identifyLookup(left.name, left.arguments);
9062
+ return identity !== void 0 && identity === identifyLookup(right.name, right.arguments);
9063
+ }
9064
+ /**
8956
9065
  * Collects the live message ids placed in records or left without a record.
8957
9066
  * @param projection - The projected records and unplaced ids
8958
9067
  * @returns The distinct live message ids
@@ -9508,17 +9617,17 @@ function matchEntities(index, text, partial) {
9508
9617
  * Projects the owner records and the rules record from a conversation's messages.
9509
9618
  *
9510
9619
  * @remarks
9511
- * Each line is a sentence of a live message, verbatim except that a sentence that opens with a pronoun opens with its party and a colon. A message is live when it is a user message,
9512
- * or a tool message whose finding is the last finding of its call, and the input neither excludes it nor
9513
- * files it quiet or replaced. A finding with an undefined `result` still replaces the earlier
9514
- * finding of the same call, so the earlier result leaves every record.
9620
+ * Each line is a sentence of a live message, verbatim except that a sentence that opens with a
9621
+ * pronoun opens with its party and a colon. {@link collectLive} retains user messages and the
9622
+ * earliest finding in each call identity's last run of identical text, subject to exclusions,
9623
+ * quiet classification, and supersessions. An undefined result still replaces the earlier finding.
9515
9624
  *
9516
9625
  * A live message joins the owner records its entities name, directly or through a linked lookup
9517
9626
  * argument. A message that names no owner joins where its earlier side of an amendment pair joins,
9518
9627
  * or on the rules record when it is filed as a rule or a correction, and is an orphan otherwise.
9519
9628
  *
9520
- * Every live sentence is retained. Accepted direct edges are listed in `amendments`. An amending message
9521
- * that is itself superseded retains its accepted edge without restoring its text.
9629
+ * Every live sentence is retained. Accepted edges are listed in `amendments`. An amender that is
9630
+ * superseded points to its newest live successor, or retains its endpoint when none exists.
9522
9631
  *
9523
9632
  * A sentence that opens with a pronoun takes the party named in the sentence before it. The reading
9524
9633
  * trusts capitals, so a capitalized word that is no person, such as a carrier named mid-sentence,
@@ -9726,12 +9835,19 @@ function splitTopic(topic) {
9726
9835
  * Cuts entries to a room and names how many it left out.
9727
9836
  *
9728
9837
  * @remarks
9729
- * The cut keeps at least one entry. When it leaves entries out, its last line reads
9838
+ * By default the cut keeps at least one entry. With `first: false`, it keeps whole entries only
9839
+ * when the joined result, tool framing, and omission line fit, and considers later entries after
9840
+ * an oversized group. When it leaves some entries out, its last line reads
9730
9841
  * `N older items not shown; name a narrower topic to narrow the recall`, which
9731
- * {@link matchesCutLine} recognizes.
9842
+ * {@link matchesCutLine} recognizes. Pass `more` when the entries aren't in age order, so the line
9843
+ * doesn't claim an omitted entry is older than a kept one. A fitted cut also uses `more` when it
9844
+ * skips an entry before a kept one. With no kept entry it reads
9845
+ * `N older items didn't fit in this request; answer from the messages you can see`.
9732
9846
  *
9733
9847
  * @param entries - The entries in the order they are kept
9734
9848
  * @param room - The estimate units the joined entries can take
9849
+ * @param kind - What the cut line calls an omitted entry. Default: `older`
9850
+ * @param first - If `true`, keeps the first entry regardless of room; if `false`, fits every entry and the omission line. Default: `true`
9735
9851
  * @returns The kept entries followed by the cut line when any were left out, joined by newlines
9736
9852
  *
9737
9853
  * @example
@@ -9739,7 +9855,30 @@ function splitTopic(topic) {
9739
9855
  * cutListing(['first entry', 'second entry'], 1) // 'first entry\n1 older item not shown; name a narrower topic to narrow the recall'
9740
9856
  * ```
9741
9857
  */
9742
- function cutListing(entries, room) {
9858
+ function cutListing(entries, room, kind = "older", first = true) {
9859
+ if (!first) {
9860
+ if (estimateMessages([{
9861
+ id: "recall",
9862
+ role: "tool",
9863
+ content: entries.join("\n")
9864
+ }]) <= room) return entries.join("\n");
9865
+ const kept = entries.reduce((accepted, entry, at) => {
9866
+ const left = entries.length - accepted.length - 1;
9867
+ const omitted = accepted.length < at ? "more" : kind;
9868
+ return estimateMessages([{
9869
+ id: "recall",
9870
+ role: "tool",
9871
+ content: [
9872
+ ...accepted.map((index) => entries[index]),
9873
+ entry,
9874
+ ...left > 0 ? [`${left} ${omitted} item${left === 1 ? "" : "s"} not shown; name a narrower topic to narrow the recall`] : []
9875
+ ].join("\n")
9876
+ }]) <= room ? [...accepted, at] : accepted;
9877
+ }, []);
9878
+ const left = entries.length - kept.length;
9879
+ const omitted = kept.some((at, index) => at !== index) ? "more" : kind;
9880
+ return [...kept.map((at) => entries[at]), ...left > 0 ? [`${left} ${omitted} item${left === 1 ? "" : "s"} ${kept.length === 0 ? "didn't fit in this request; answer from the messages you can see" : "not shown; name a narrower topic to narrow the recall"}`] : []].join("\n");
9881
+ }
9743
9882
  const stop = [...entries.keys()].findIndex((at) => at > 0 && estimateMessages([{
9744
9883
  id: "recall",
9745
9884
  role: "tool",
@@ -9747,7 +9886,7 @@ function cutListing(entries, room) {
9747
9886
  }]) > room);
9748
9887
  const kept = entries.slice(0, stop === -1 ? void 0 : stop);
9749
9888
  const left = entries.length - kept.length;
9750
- return [...kept, ...left > 0 ? [`${left} older item${left === 1 ? "" : "s"} not shown; name a narrower topic to narrow the recall`] : []].join("\n");
9889
+ return [...kept, ...left > 0 ? [`${left} ${kind} item${left === 1 ? "" : "s"} not shown; name a narrower topic to narrow the recall`] : []].join("\n");
9751
9890
  }
9752
9891
  /**
9753
9892
  * Checks whether a line is the cut line {@link cutListing} writes.
@@ -9761,7 +9900,7 @@ function cutListing(entries, room) {
9761
9900
  * ```
9762
9901
  */
9763
9902
  function matchesCutLine(line) {
9764
- return /^\d+ older items? not shown; /.test(line);
9903
+ return /^\d+ (?:older|more) items? (?:not shown; |didn't fit in this request; answer from the messages you can see$)/.test(line);
9765
9904
  }
9766
9905
  /**
9767
9906
  * Renders the tail stub of a lookup result from an earlier request.
@@ -9822,11 +9961,12 @@ function fitSlope(groups) {
9822
9961
  * Collects the ids of the live messages in conversation order.
9823
9962
  *
9824
9963
  * @remarks
9825
- * A message is live when it is a user message, or a tool message whose finding is the last finding of its
9826
- * call, and the input neither excludes it nor files it quiet or replaced. A finding replaces any
9827
- * earlier finding of the same call, an empty one included, so an empty lookup leaves the earlier
9828
- * result out of every record. A finding with no canonical identity stays live as its own source
9829
- * and participates in no replacement.
9964
+ * A message is live when it is a user message or the first unsuperseded finding in its canonical
9965
+ * call identity's last run of identical text, and the input neither excludes it nor files it quiet
9966
+ * or superseded. When the whole run is superseded, its first finding reaches that final filter.
9967
+ * Different text or an undefined result replaces the earlier run, so an empty lookup leaves the
9968
+ * earlier result out of every record. A thrown lookup leaves the earlier result live. A finding
9969
+ * with no canonical identity participates in no replacement.
9830
9970
  *
9831
9971
  * @param input - The messages, findings, and classification to read
9832
9972
  * @returns The live message ids
@@ -9842,7 +9982,14 @@ function collectLive(input) {
9842
9982
  const position = new Map(input.messages.map((message, at) => [message.id, at]));
9843
9983
  const findings = input.findings.filter((finding) => position.has(finding.id));
9844
9984
  const identities = new Map(findings.map((finding) => [finding.id, identifyLookup(finding.name, finding.arguments)]));
9845
- const current = new Set(findings.filter((finding) => finding.result !== void 0 && !findings.some((later) => identities.get(finding.id) !== void 0 && (position.get(later.id) ?? -1) > (position.get(finding.id) ?? -1) && identities.get(later.id) === identities.get(finding.id))).map((finding) => finding.id));
9985
+ const current = new Set([...Map.groupBy(findings, (finding) => identities.get(finding.id) ?? finding).values()].flatMap((group) => {
9986
+ const ordered = group.toSorted((left, right) => (position.get(left.id) ?? -1) - (position.get(right.id) ?? -1));
9987
+ const last = ordered.at(-1);
9988
+ if (last?.result === void 0) return [];
9989
+ const boundary = ordered.findLastIndex((finding) => finding.result === void 0 || finding.text !== last.text);
9990
+ const first = ordered.slice(boundary + 1).find((finding) => (classification.supersessions.get(finding.id) ?? []).length === 0) ?? ordered[boundary + 1];
9991
+ return first === void 0 ? [] : [first.id];
9992
+ }));
9846
9993
  return [...new Map(input.messages.map((message) => [message.id, message])).values()].filter((message) => message.role === "user" || message.role === "tool" && current.has(message.id)).map((message) => message.id).filter((id) => !excluded.has(id) && !classification.quiet.has(id) && (classification.supersessions.get(id) ?? []).length === 0);
9847
9994
  }
9848
9995
  /**
@@ -9897,17 +10044,18 @@ function placeMember(input, links, amended, id, seen, settled) {
9897
10044
  return keys;
9898
10045
  }
9899
10046
  /**
9900
- * Collects accepted direct amendments of live messages.
10047
+ * Collects accepted amendments of live messages, following superseded amenders to live successors.
9901
10048
  *
9902
10049
  * @remarks
9903
10050
  * An accepted edge relates an earlier live message to an amender without claiming which sentences
9904
- * changed. An amender takes effect while live and retains its edge after another message supersedes
9905
- * it, without restoring the superseded text. An excluded or quiet user or tool message has no effect.
10051
+ * changed. A superseded amender points to its newest reachable live successor in conversation order,
10052
+ * with cycles guarded and duplicate endpoints removed. With no live successor, its endpoint stays.
10053
+ * This never restores superseded text. An excluded or quiet user or tool message has no effect.
9906
10054
  *
9907
10055
  * @param input - The exclusions and classification to read
9908
10056
  * @param byId - The messages by id
9909
10057
  * @param live - The live message ids from {@link collectLive}
9910
- * @returns The distinct accepted direct edges in conversation order
10058
+ * @returns The distinct accepted edges in conversation order
9911
10059
  *
9912
10060
  * @example
9913
10061
  * ```ts
@@ -9921,7 +10069,10 @@ function collectAmendments(input, byId, live) {
9921
10069
  const ordered = [...new Set(input.messages.map((message) => message.id))];
9922
10070
  const positions = new Map(ordered.map((id, index) => [id, index]));
9923
10071
  const effective = /* @__PURE__ */ new Set([...live, ...[...byId.values()].filter((message) => message.role !== "assistant" && !excluded.has(message.id) && !classification.quiet.has(message.id) && (classification.supersessions.get(message.id) ?? []).length > 0).map((message) => message.id)]);
9924
- return ordered.filter((id) => living.has(id)).flatMap((earlier) => [...new Set(classification.amendments.get(earlier) ?? [])].filter((later) => effective.has(later) && positions.has(later)).sort((a, b) => (positions.get(a) ?? 0) - (positions.get(b) ?? 0)).map((later) => ({
10072
+ return ordered.filter((id) => living.has(id)).flatMap((earlier) => [...new Set((classification.amendments.get(earlier) ?? []).filter((later) => effective.has(later) && positions.has(later)).map((later) => {
10073
+ const successors = new Set(scanAmendments(classification.supersessions, later, () => true));
10074
+ return ordered.findLast((id) => living.has(id) && successors.has(id)) ?? later;
10075
+ }))].sort((a, b) => (positions.get(a) ?? 0) - (positions.get(b) ?? 0)).map((later) => ({
9925
10076
  earlier,
9926
10077
  later
9927
10078
  })));
@@ -9967,6 +10118,83 @@ function buildLines(input, byId, id, holders, system) {
9967
10118
  }];
9968
10119
  });
9969
10120
  }
10121
+ /**
10122
+ * Encodes a sentence's record placement without comparing rendered text.
10123
+ * @param line - The record key, title, source, and zero-based sentence index
10124
+ * @returns The serialized placement identity
10125
+ * @example
10126
+ * ```ts
10127
+ * buildLedgerLineKey({ key: 'rules', title: 'Rules', source: 'one', sentence: 0 })
10128
+ * ```
10129
+ */
10130
+ function buildLedgerLineKey(line) {
10131
+ return JSON.stringify([
10132
+ line.key,
10133
+ line.title,
10134
+ line.source,
10135
+ line.sentence
10136
+ ]);
10137
+ }
10138
+ /**
10139
+ * Collects the record's sentence receipts not already held by a window.
10140
+ * @param record - The selected record with its original sentence identities
10141
+ * @param held - Placement identities already rendered in the window
10142
+ * @returns The remaining sentences with their record placement
10143
+ * @example
10144
+ * ```ts
10145
+ * collectLedgerLines({ key: 'rules', title: 'Rules', members: [], lines: [] }, new Set()) // []
10146
+ * ```
10147
+ */
10148
+ function collectLedgerLines(record, held) {
10149
+ return record.lines.map((line) => ({
10150
+ ...line,
10151
+ key: record.key,
10152
+ title: record.title
10153
+ })).filter((line) => !held.has(buildLedgerLineKey(line)));
10154
+ }
10155
+ /**
10156
+ * Selects each marked request's unambiguous final call-free reply.
10157
+ * @param messages - Stored conversation messages in order
10158
+ * @param requests - Marked request ids delimiting reply intervals
10159
+ * @param notes - Ledger notes that separate answer passes
10160
+ * @returns Eligible final replies keyed by request id
10161
+ * @example
10162
+ * ```ts
10163
+ * collectLedgerReplies([], new Set(), new Set()).size // 0
10164
+ * ```
10165
+ */
10166
+ function collectLedgerReplies(messages, requests, notes) {
10167
+ const positions = messages.flatMap((message, at) => requests.has(message.id) ? [at] : []);
10168
+ return new Map(positions.flatMap((position, index) => {
10169
+ const request = messages[position];
10170
+ const turns = messages.slice(position + 1, positions[index + 1]);
10171
+ const reply = turns.findLast((message) => message.role === "assistant" && !hasCalls(message));
10172
+ const answers = turns.flatMap((message, at) => message.role === "assistant" && !hasCalls(message) && message.content.trim() !== "" ? [at] : []);
10173
+ const certain = answers.every((at, offset) => offset === 0 || turns.slice((answers[offset - 1] ?? -1) + 1, at).some((message) => notes.has(message.id)));
10174
+ return request !== void 0 && certain && reply !== void 0 && reply.content.trim() !== "" ? [[request.id, reply]] : [];
10175
+ }));
10176
+ }
10177
+ /**
10178
+ * Captures source bytes and direct amendment edges for a held window.
10179
+ * @param messages - Current stored messages
10180
+ * @param sources - Requests, replies, and record sources the window holds
10181
+ * @param amendments - Accepted direct amendment edges from the current projection
10182
+ * @returns Source and edge identities for stale detection
10183
+ * @example
10184
+ * ```ts
10185
+ * collectLedgerEvidence([], new Set(), []).messages.size // 0
10186
+ * ```
10187
+ */
10188
+ function collectLedgerEvidence(messages, sources, amendments) {
10189
+ return {
10190
+ messages: new Set(messages.filter((message) => sources.has(message.id)).map((message) => JSON.stringify(["message", message]))),
10191
+ amendments: new Map(amendments.filter((edge) => sources.has(edge.earlier) || sources.has(edge.later)).map((edge) => [JSON.stringify([
10192
+ "edge",
10193
+ edge.earlier,
10194
+ edge.later
10195
+ ]), { ...edge }]))
10196
+ };
10197
+ }
9970
10198
  // src/core/ledgers/Classifier.ts
9971
10199
  /**
9972
10200
  * Files messages through the conversation's judgment manager and reads their categories and corrections.
@@ -10011,7 +10239,7 @@ var Classifier = class {
10011
10239
  /**
10012
10240
  * Reads the category a message is filed under.
10013
10241
  * @param id - The message id
10014
- * @returns The assigned category, the recorded category at the cutoff, or `undefined`
10242
+ * @returns The assigned category, the recorded category at the cutoff, or `undefined`; a quiet winner suppressed by the user literal floor remains undecided
10015
10243
  */
10016
10244
  category(id) {
10017
10245
  const message = this.#options.conversation.message(id);
@@ -10020,25 +10248,29 @@ var Classifier = class {
10020
10248
  if (assigned !== void 0) return assigned;
10021
10249
  const answer = this.#readCategories(id);
10022
10250
  const cutoff = this.#options.thresholds.category;
10023
- return LEDGER_CATEGORIES.flatMap((category) => {
10251
+ const winner = LEDGER_CATEGORIES.flatMap((category) => {
10024
10252
  const probability = answer?.probabilities[category];
10025
10253
  return probability !== void 0 && probability >= cutoff ? [{
10026
10254
  category,
10027
10255
  probability
10028
10256
  }] : [];
10029
10257
  }).reduce((best, candidate) => best === void 0 || candidate.probability > best.probability ? candidate : best, void 0)?.category;
10258
+ return winner !== void 0 && QUIET_CATEGORIES.includes(winner) && !this.quiet(id) ? void 0 : winner;
10030
10259
  }
10031
10260
  /**
10032
10261
  * Reports whether a message files under a quiet category.
10033
10262
  * @param id - The message id
10034
- * @returns True if the assigned or recorded category is quiet; false otherwise.
10263
+ * @returns True if the assigned category is quiet or the recorded quiet weight reaches the cutoff, except for a user message carrying an id or number; false otherwise
10035
10264
  */
10036
10265
  quiet(id) {
10037
10266
  const message = this.#options.conversation.message(id);
10038
10267
  if (message === void 0) return false;
10039
10268
  const assigned = this.#options.assign(message);
10040
10269
  if (assigned !== void 0) return QUIET_CATEGORIES.includes(assigned);
10041
- return this.#weigh(id, QUIET_CATEGORIES) >= this.#options.thresholds.category;
10270
+ if (this.#weigh(id, QUIET_CATEGORIES) < this.#options.thresholds.category) return false;
10271
+ if (message.role !== "user") return true;
10272
+ const literals = extractLiterals(message.content);
10273
+ return literals.ids.size + literals.numbers.size === 0;
10042
10274
  }
10043
10275
  /**
10044
10276
  * Reports whether a message files under a decisive category.
@@ -10077,7 +10309,7 @@ var Classifier = class {
10077
10309
  if (head !== "amends" && head !== "supersedes" || !(0, _orkestrel_contract.isString)(earlier) || !(0, _orkestrel_contract.isString)(later)) return [];
10078
10310
  const before = this.#options.conversation.message(earlier);
10079
10311
  const after = this.#options.conversation.message(later);
10080
- if (before === void 0 || after === void 0 || (this.#readNoul(this.#buildPair(head, earlier, later)) ?? 0) < this.#options.thresholds[head]) return [];
10312
+ if (before === void 0 || after === void 0 || (this.#readNoul(this.#buildPair(head, earlier, later)) ?? 0) < this.#options.thresholds[head] || this.quiet(later)) return [];
10081
10313
  return head === "amends" && !this.#sharesLiteral(extractLiterals(before.content), extractLiterals(after.content)) ? [] : [{
10082
10314
  head,
10083
10315
  earlier,
@@ -10329,8 +10561,9 @@ var Classifier = class {
10329
10561
  * has left.
10330
10562
  *
10331
10563
  * @remarks
10332
- * `observe` rescales from the first call of each finished request with the overhead cost taken out,
10333
- * and keeps that request's calls for the marginal rate and the longest final completion for the reply
10564
+ * `observe` rescales from the first call only when `spared` is absent, zero, or at least `overhead`,
10565
+ * with `max(0, overhead - spared)` taken out. A partially spared call keeps the existing scale.
10566
+ * It keeps that request's calls for the marginal rate and the longest final completion for the reply
10334
10567
  * reserve after subtracting its thinking. `overhead` never changes after construction.
10335
10568
  * {@link GaugeOptions} defines the measured use, reply reserve, and recall-room formulas.
10336
10569
  * {@link LedgerOptions} defines the ledger's plan budget and recall close rule.
@@ -10388,7 +10621,7 @@ var Gauge = class {
10388
10621
  }
10389
10622
  remainder(calls) {
10390
10623
  const call = calls.at(-1);
10391
- const used = (0, _orkestrel_contract.isFiniteNumber)(call?.prompt) ? call.prompt + (call.completion ?? 0) - (this.#replay === "none" ? call.thinking ?? 0 : 0) : this.#overhead + this.#scale * (call?.estimate ?? 0);
10624
+ const used = (0, _orkestrel_contract.isFiniteNumber)(call?.prompt) ? call.prompt + (call.completion ?? 0) - (this.#replay === "none" ? call.thinking ?? 0 : 0) : Math.max(0, this.#overhead - (call?.spared ?? 0)) + this.#scale * (call?.estimate ?? 0);
10392
10625
  return Math.max(0, this.#capacity - used);
10393
10626
  }
10394
10627
  reserve(calls, longest) {
@@ -10408,8 +10641,8 @@ var Gauge = class {
10408
10641
  }
10409
10642
  observe(calls, reply) {
10410
10643
  const first = calls[0];
10411
- if (first !== void 0 && (0, _orkestrel_contract.isFiniteNumber)(first.prompt) && first.estimate > 0) {
10412
- const priced = first.prompt - this.#overhead;
10644
+ if (first !== void 0 && (0, _orkestrel_contract.isFiniteNumber)(first.prompt) && first.estimate > 0 && ((first.spared ?? 0) === 0 || (first.spared ?? 0) >= this.#overhead)) {
10645
+ const priced = first.prompt - Math.max(0, this.#overhead - (first.spared ?? 0));
10413
10646
  if (priced > 0) this.#scale = priced / first.estimate;
10414
10647
  }
10415
10648
  this.#history = [...this.#history, [...calls]];
@@ -10586,10 +10819,13 @@ var LedgerRequest = class {
10586
10819
  #faults = /* @__PURE__ */ new Set();
10587
10820
  #message;
10588
10821
  #plan;
10822
+ #scope;
10823
+ #draft;
10824
+ #sent;
10589
10825
  #passes = [];
10590
10826
  #attempts = [];
10591
10827
  #admitted = /* @__PURE__ */ new Set();
10592
- #listed = /* @__PURE__ */ new Map();
10828
+ #listed = [];
10593
10829
  #recalls = 0;
10594
10830
  #closed = false;
10595
10831
  /**
@@ -10620,6 +10856,16 @@ var LedgerRequest = class {
10620
10856
  get plan() {
10621
10857
  return this.#plan;
10622
10858
  }
10859
+ get scope() {
10860
+ return this.#scope;
10861
+ }
10862
+ apply(scope) {
10863
+ this.#scope = scope;
10864
+ }
10865
+ /** Returns the copy and receipts adopted by the last provider turn. */
10866
+ get sent() {
10867
+ return this.#sent;
10868
+ }
10623
10869
  /**
10624
10870
  * Returns the selection the current pass's run adopted last.
10625
10871
  *
@@ -10675,6 +10921,8 @@ var LedgerRequest = class {
10675
10921
  this.#passes.at(-1)?.adopt(selection, boundary);
10676
10922
  }
10677
10923
  observe(call, position) {
10924
+ const draft = this.#draft;
10925
+ this.#sent = draft !== void 0 && this.selection?.messages.some((message) => message.id === draft.copy.id && message.content === draft.copy.content) ? draft : void 0;
10678
10926
  this.#passes.at(-1)?.observe(call, position);
10679
10927
  }
10680
10928
  spend(usage) {
@@ -10695,8 +10943,9 @@ var LedgerRequest = class {
10695
10943
  faulted(error) {
10696
10944
  return this.#faults.has(error);
10697
10945
  }
10698
- file(attempts, plan) {
10946
+ file(attempts, plan, draft) {
10699
10947
  this.#attempts = [...this.#attempts, ...attempts];
10948
+ this.#draft = draft;
10700
10949
  if (plan !== void 0) this.#plan = plan;
10701
10950
  }
10702
10951
  admit(name, args) {
@@ -10713,11 +10962,24 @@ var LedgerRequest = class {
10713
10962
  close() {
10714
10963
  this.#closed = true;
10715
10964
  }
10716
- list(result, lines) {
10717
- this.#listed = new Map([...this.#listed, [result, lines]]);
10965
+ list(result, lines, groups) {
10966
+ const accepted = groups.reduce((kept, group) => {
10967
+ const offset = kept.length === 0 ? 0 : kept.join("\n").length + 1;
10968
+ return result.slice(offset) === group || result.slice(offset).startsWith(`${group}\n`) ? [...kept, group] : kept;
10969
+ }, []);
10970
+ this.#listed = [...this.#listed, {
10971
+ result,
10972
+ lines,
10973
+ groups: accepted,
10974
+ turn: this.calls.length
10975
+ }];
10976
+ }
10977
+ /** Returns recall results with their accepted whole groups and provider-turn positions. */
10978
+ get listings() {
10979
+ return this.#listed;
10718
10980
  }
10719
10981
  lines(result) {
10720
- return this.#listed.get(result);
10982
+ return this.#listed.findLast((listing) => listing.result === result)?.lines;
10721
10983
  }
10722
10984
  };
10723
10985
  // src/core/ledgers/LedgerScan.ts
@@ -10828,6 +11090,29 @@ var LedgerScan = class {
10828
11090
  }
10829
11091
  };
10830
11092
  // src/core/ledgers/shapers.ts
11093
+ /** Describes the minimal serialized evidence of a nonempty exchange window. */
11094
+ var ledgerWindowShape = (0, _orkestrel_contract.objectShape)({
11095
+ digest: (0, _orkestrel_contract.stringShape)(),
11096
+ exchanges: (0, _orkestrel_contract.arrayShape)((0, _orkestrel_contract.objectShape)({
11097
+ request: (0, _orkestrel_contract.stringShape)(),
11098
+ copy: (0, _orkestrel_contract.optionalShape)((0, _orkestrel_contract.stringShape)()),
11099
+ reply: (0, _orkestrel_contract.stringShape)(),
11100
+ lines: (0, _orkestrel_contract.arrayShape)((0, _orkestrel_contract.objectShape)({
11101
+ key: (0, _orkestrel_contract.stringShape)(),
11102
+ title: (0, _orkestrel_contract.stringShape)(),
11103
+ source: (0, _orkestrel_contract.stringShape)(),
11104
+ sentence: (0, _orkestrel_contract.numberShape)({
11105
+ integer: true,
11106
+ min: 0,
11107
+ max: Number.MAX_SAFE_INTEGER
11108
+ })
11109
+ }))
11110
+ }), { min: 1 }),
11111
+ amendments: (0, _orkestrel_contract.arrayShape)((0, _orkestrel_contract.objectShape)({
11112
+ earlier: (0, _orkestrel_contract.stringShape)(),
11113
+ later: (0, _orkestrel_contract.stringShape)()
11114
+ }))
11115
+ });
10831
11116
  /**
10832
11117
  * Describes a ledger snapshot exactly: an unknown key refuses, so a key a later version adds never reads
10833
11118
  * as absent.
@@ -10845,13 +11130,36 @@ var ledgerSnapshotShape = (0, _orkestrel_contract.objectShape)({
10845
11130
  gauge: (0, _orkestrel_contract.optionalShape)((0, _orkestrel_contract.objectShape)({
10846
11131
  scale: (0, _orkestrel_contract.numberShape)(),
10847
11132
  overhead: (0, _orkestrel_contract.numberShape)()
10848
- }))
11133
+ })),
11134
+ window: (0, _orkestrel_contract.optionalShape)(ledgerWindowShape)
10849
11135
  });
10850
11136
  // src/core/ledgers/contracts.ts
10851
- /** Reads the faults of a refused ledger snapshot. */
11137
+ /** Reads structural faults of a serialized exchange window. */
11138
+ var ledgerWindowContract = (0, _orkestrel_contract.createContract)(ledgerWindowShape);
11139
+ /** Reads structural faults of a ledger snapshot, including its optional held exchange window. */
10852
11140
  var ledgerSnapshotContract = (0, _orkestrel_contract.createContract)(ledgerSnapshotShape);
10853
11141
  // src/core/ledgers/validators.ts
10854
11142
  /**
11143
+ * Checks whether a value has the exact shape of a nonempty exchange window.
11144
+ * @param value - The untrusted window
11145
+ * @returns True if the window and its members are well shaped; false otherwise
11146
+ * @example
11147
+ * ```ts
11148
+ * isLedgerWindowSnapshot({ digest: 'checksum', exchanges: [{ request: 'request', reply: 'reply', lines: [] }], amendments: [] }) // true
11149
+ * ```
11150
+ */
11151
+ function isLedgerWindowSnapshot(value) {
11152
+ return (0, _orkestrel_contract.holds)(() => {
11153
+ if (!(0, _orkestrel_contract.isRecord)(value) || Object.keys(value).length !== 3 || !(0, _orkestrel_contract.isString)(value.digest)) return false;
11154
+ const { exchanges, amendments } = value;
11155
+ if (!(0, _orkestrel_contract.arrayOf)(_orkestrel_contract.isRecord)(exchanges) || exchanges.length === 0 || !(0, _orkestrel_contract.arrayOf)(_orkestrel_contract.isRecord)(amendments)) return false;
11156
+ return exchanges.every((entry) => {
11157
+ const { request, reply, copy, lines } = entry;
11158
+ return Object.keys(entry).length === 3 + Number(copy !== void 0) && (0, _orkestrel_contract.isString)(request) && (0, _orkestrel_contract.isString)(reply) && (copy === void 0 || (0, _orkestrel_contract.isString)(copy)) && (0, _orkestrel_contract.arrayOf)(_orkestrel_contract.isRecord)(lines) && lines.every((line) => Object.keys(line).length === 4 && (0, _orkestrel_contract.isString)(line.key) && (0, _orkestrel_contract.isString)(line.title) && (0, _orkestrel_contract.isString)(line.source) && isNonNegativeSafeInteger(line.sentence));
11159
+ }) && amendments.every((edge) => Object.keys(edge).length === 2 && (0, _orkestrel_contract.isString)(edge.earlier) && (0, _orkestrel_contract.isString)(edge.later));
11160
+ });
11161
+ }
11162
+ /**
10855
11163
  * Checks whether a value is finite and lies in the interval (0, 1].
10856
11164
  * @param value - The value to inspect
10857
11165
  * @returns True if the value is a valid ledger fraction; false otherwise
@@ -10902,6 +11210,8 @@ function isLedgerQuestion(value) {
10902
11210
  * guard is structural: it admits a conversation holding a stored `system` role
10903
11211
  * or a repeated id, which `requireConversationSnapshot` and the `Conversation` constructor then
10904
11212
  * refuse as the stored-data check a ledger's hydrate runs.
11213
+ * A present `window` must be nonempty and exactly shaped. This guard does not audit its references
11214
+ * or sentence ranges. Hydration discards the window when its digest disagrees or cannot be computed.
10905
11215
  *
10906
11216
  * @param value - The value to inspect, such as a parsed checkpoint
10907
11217
  * @returns True if the value has the shape of a ledger snapshot whose marked ids name messages of its own conversation; false otherwise
@@ -10915,8 +11225,8 @@ function isLedgerQuestion(value) {
10915
11225
  function isLedgerSnapshot(value) {
10916
11226
  return (0, _orkestrel_contract.holds)(() => {
10917
11227
  if (!(0, _orkestrel_contract.isRecord)(value)) return false;
10918
- const { conversation, requests, notes, failures, gauge } = value;
10919
- if (Object.keys(value).length !== (gauge === void 0 ? 4 : 5)) return false;
11228
+ const { conversation, requests, notes, failures, gauge, window } = value;
11229
+ if (Object.keys(value).length !== 4 + Number(gauge !== void 0) + Number(window !== void 0)) return false;
10920
11230
  if (!isConversationSnapshot(conversation)) return false;
10921
11231
  const isIds = (0, _orkestrel_contract.arrayOf)(_orkestrel_contract.isString);
10922
11232
  if (!isIds(requests) || !isIds(notes) || !isIds(failures)) return false;
@@ -10926,7 +11236,7 @@ function isLedgerSnapshot(value) {
10926
11236
  if (!(0, _orkestrel_contract.isFiniteNumber)(scale) || scale <= 0 || !(0, _orkestrel_contract.isFiniteNumber)(overhead) || overhead < 0) return false;
10927
11237
  }
10928
11238
  const roles = new Map(conversation.messages.map((message) => [message.id, message.role]));
10929
- return requests.every((id) => roles.get(id) === "user") && notes.every((id) => roles.get(id) === "user") && failures.every((id) => roles.get(id) === "tool");
11239
+ return requests.every((id) => roles.get(id) === "user") && notes.every((id) => roles.get(id) === "user") && failures.every((id) => roles.get(id) === "tool") && (window === void 0 || isLedgerWindowSnapshot(window));
10930
11240
  });
10931
11241
  }
10932
11242
  // src/core/ledgers/Ledger.ts
@@ -10956,6 +11266,8 @@ var Ledger = class {
10956
11266
  #scans = /* @__PURE__ */ new Set();
10957
11267
  #calibrations = /* @__PURE__ */ new Set();
10958
11268
  #gauge;
11269
+ #window = [];
11270
+ #evidence = collectLedgerEvidence([], /* @__PURE__ */ new Set(), []);
10959
11271
  /**
10960
11272
  * Composes the conversation, classifier, tools, and agent.
10961
11273
  * @param provider - The provider that serves the conversation
@@ -10989,7 +11301,8 @@ var Ledger = class {
10989
11301
  if (!isLedgerQuestion(options.questions)) throw new LedgerError("QUESTION", "questions must hold a choice category question whose criteria describe every category, a non-empty topic string, and noul amends and supersedes questions; pass the LEDGER_QUESTIONS constant for the measured wording");
10990
11302
  const snapshot = options.snapshot;
10991
11303
  if (snapshot !== void 0 && !isLedgerSnapshot(snapshot)) {
10992
- const faults = ledgerSnapshotContract.audit(snapshot);
11304
+ const candidate = options.snapshot;
11305
+ const faults = ledgerSnapshotContract.audit(candidate);
10993
11306
  throw new LedgerError("SNAPSHOT", `snapshot must be well formed and mark only messages of its conversation; pass what snapshot() returned${faults.length === 0 ? "" : `\n${(0, _orkestrel_contract.describeFaults)(faults, ledgerSnapshotContract.schema)}`}`, { context: { faults } });
10994
11307
  }
10995
11308
  this.#provider = provider;
@@ -11037,7 +11350,7 @@ var Ledger = class {
11037
11350
  }));
11038
11351
  tools.add((0, _orkestrel_tool.createTool)({
11039
11352
  name: "recall",
11040
- description: `Recall what the full conversation record holds on a topic: an owner name, an id, or one of the topics ${renderTopicNames(options.topics)}. Returns source lines, newest first.`,
11353
+ description: `Find something said earlier that isn't in the messages you can see, by an owner name, an id, or one of the topics ${renderTopicNames(options.topics)}. Returns statements and lookup findings first, then earlier requests and replies labeled by origin; newest first within each group.`,
11041
11354
  contract: (0, _orkestrel_contract.objectShape)({ topic: (0, _orkestrel_contract.stringShape)({
11042
11355
  min: 1,
11043
11356
  description: "An owner name, an id, or a topic"
@@ -11121,8 +11434,8 @@ var Ledger = class {
11121
11434
  }
11122
11435
  }
11123
11436
  /**
11124
- * Serializes the conversation, the marked ids, and the gauge into a snapshot a later ledger hydrates from.
11125
- * @returns The snapshot, each id list in conversation order and `gauge` present after the ledger holds one
11437
+ * Serializes the conversation, marked ids, gauge, and completed exchange window for a later ledger.
11438
+ * @returns The snapshot, each id list in conversation order, with a gauge and window only when held
11126
11439
  * @throws {AgentError} Thrown when a request or calibration is active (code `'CONCURRENCY'`)
11127
11440
  */
11128
11441
  snapshot() {
@@ -11131,12 +11444,31 @@ var Ledger = class {
11131
11444
  const conversation = this.#conversation.snapshot();
11132
11445
  const ids = conversation.messages.map((message) => message.id);
11133
11446
  const gauge = this.gauge;
11447
+ const window = {
11448
+ exchanges: this.#window.map((entry) => ({
11449
+ request: entry.request.id,
11450
+ ...entry.request.content === this.#conversation.message(entry.request.id)?.content ? {} : { copy: entry.request.content },
11451
+ reply: entry.reply.id,
11452
+ lines: entry.lines.map(({ key, title, source, sentence }) => ({
11453
+ key,
11454
+ title,
11455
+ source,
11456
+ sentence
11457
+ }))
11458
+ })),
11459
+ amendments: [...this.#evidence.amendments.values()].map((edge) => ({ ...edge }))
11460
+ };
11461
+ const digest = window.exchanges.length === 0 ? void 0 : digestLedgerWindow(window.exchanges, conversation.messages);
11134
11462
  return {
11135
11463
  conversation,
11136
11464
  requests: ids.filter((id) => this.#requests.has(id)),
11137
11465
  notes: ids.filter((id) => this.#annotations.has(id)),
11138
11466
  failures: ids.filter((id) => this.#results.get(id)?.success === false),
11139
- ...gauge === void 0 ? {} : { gauge }
11467
+ ...gauge === void 0 ? {} : { gauge },
11468
+ ...window.exchanges.length === 0 || digest === void 0 ? {} : { window: {
11469
+ ...window,
11470
+ digest
11471
+ } }
11140
11472
  };
11141
11473
  }
11142
11474
  /**
@@ -11154,6 +11486,7 @@ var Ledger = class {
11154
11486
  if (this.#busy()) throw new AgentError("CONCURRENCY", "a ledger request is already active");
11155
11487
  const caller = (0, _orkestrel_abort.createAbort)({ ...signal === void 0 ? {} : { signal } }).signal;
11156
11488
  const request = new LedgerRequest(content, this.#options.recall?.limit);
11489
+ const scope = this.#agent.context.scope;
11157
11490
  this.#live.add(request);
11158
11491
  try {
11159
11492
  if (this.#gauge === void 0) await this.#measureGauge(caller);
@@ -11169,17 +11502,18 @@ var Ledger = class {
11169
11502
  });
11170
11503
  const passes = [first];
11171
11504
  if (!caller.aborted && (first.partial || first.content.trim() === "")) {
11172
- const digest = this.#buildDigest(request, entered);
11173
- if (digest !== void 0) this.#annotations.add(this.#conversation.add({
11505
+ const recovery = this.#buildDigest(request, entered);
11506
+ if (recovery.digest !== void 0) this.#annotations.add(this.#conversation.add({
11174
11507
  role: "user",
11175
- content: digest
11508
+ content: recovery.digest
11176
11509
  }).id);
11177
11510
  this.#annotations.add(this.#conversation.add({
11178
11511
  role: "user",
11179
11512
  content: LEDGER_NOTES.cue
11180
11513
  }).id);
11181
11514
  const previous = this.#agent.context.scope;
11182
- this.#agent.context.apply(createScope({
11515
+ const answer = createScope({
11516
+ ...previous,
11183
11517
  name: "answer",
11184
11518
  tools: [],
11185
11519
  select: async (conversation, message, runSignal) => {
@@ -11187,14 +11521,15 @@ var Ledger = class {
11187
11521
  if (selection.fault !== void 0) return selection;
11188
11522
  return {
11189
11523
  ...selection,
11190
- messages: selection.messages.filter((one) => one.role !== "tool" && !hasCalls(one))
11524
+ messages: selection.messages.filter((one) => one.role !== "tool" && !hasCalls(one) && !recovery.omitted.has(one.id))
11191
11525
  };
11192
11526
  }
11193
- }));
11527
+ });
11528
+ this.#agent.context.apply(answer);
11194
11529
  try {
11195
11530
  passes.push(await this.#generatePass(request, caller, false));
11196
11531
  } finally {
11197
- this.#agent.context.apply(previous);
11532
+ if (this.#agent.context.scope === answer) this.#agent.context.apply(previous);
11198
11533
  }
11199
11534
  }
11200
11535
  const last = passes.at(-1) ?? first;
@@ -11213,7 +11548,9 @@ var Ledger = class {
11213
11548
  ...last.stop === void 0 ? {} : { stop: last.stop }
11214
11549
  };
11215
11550
  } finally {
11551
+ if (request.scope !== void 0 && this.#agent.context.scope === request.scope) this.#agent.context.apply(scope);
11216
11552
  this.#flush();
11553
+ this.#commitExchange(request);
11217
11554
  this.#live.delete(request);
11218
11555
  }
11219
11556
  }
@@ -11276,6 +11613,37 @@ var Ledger = class {
11276
11613
  for (const id of snapshot.requests) this.#requests.add(id);
11277
11614
  for (const id of snapshot.notes) this.#annotations.add(id);
11278
11615
  const messages = this.#conversation.messages();
11616
+ if (snapshot.window !== void 0 && digestLedgerWindow(snapshot.window.exchanges, messages) === snapshot.window.digest) {
11617
+ const window = snapshot.window.exchanges.map((entry) => {
11618
+ const request = this.#conversation.message(entry.request);
11619
+ const reply = this.#conversation.message(entry.reply);
11620
+ if (request === void 0 || reply === void 0) throw new LedgerError("SNAPSHOT", "window pair must belong to its conversation");
11621
+ const lines = entry.lines.map((line) => {
11622
+ const source = this.#conversation.message(line.source);
11623
+ if (source === void 0) throw new LedgerError("SNAPSHOT", "window source must belong to its conversation");
11624
+ return {
11625
+ ...line,
11626
+ text: splitSentences(source.content)[line.sentence] ?? "",
11627
+ topics: [],
11628
+ role: source.role
11629
+ };
11630
+ });
11631
+ return {
11632
+ request: {
11633
+ ...request,
11634
+ content: entry.copy ?? request.content
11635
+ },
11636
+ reply,
11637
+ lines
11638
+ };
11639
+ });
11640
+ this.#window = window;
11641
+ this.#evidence = collectLedgerEvidence(messages, new Set(this.#window.flatMap((entry) => [
11642
+ entry.request.id,
11643
+ entry.reply.id,
11644
+ ...entry.lines.map((line) => line.source)
11645
+ ])), snapshot.window.amendments);
11646
+ }
11279
11647
  const groups = collectToolGroups(messages);
11280
11648
  for (const message of messages) {
11281
11649
  if (!snapshot.failures.includes(message.id)) continue;
@@ -11406,20 +11774,26 @@ var Ledger = class {
11406
11774
  const { attempts, failure, ...filed } = classified;
11407
11775
  const planned = (0, _orkestrel_contract.attempt)(() => {
11408
11776
  if (filed.fault !== void 0) throw filed.fault;
11777
+ const { draft, ...plan } = this.#plan(selected, this.#rescan());
11409
11778
  return {
11410
- ...this.#plan(selected, this.#rescan()),
11411
- ...filed,
11412
- ...failure === void 0 ? {} : { fault: failure }
11779
+ draft,
11780
+ selection: {
11781
+ ...plan,
11782
+ ...filed,
11783
+ ...failure === void 0 ? {} : { fault: failure }
11784
+ }
11413
11785
  };
11414
11786
  });
11415
- request.file(attempts, planned.success ? planned.value : void 0);
11787
+ request.file(attempts, planned.success ? planned.value.selection : void 0, planned.success ? planned.value.draft : void 0);
11416
11788
  if (!planned.success) return this.#refuse(planned.error, filed, this.#conversation);
11789
+ const selection = planned.value.selection;
11417
11790
  return this.#annotations.has(message.id) ? {
11418
- ...planned.value,
11419
- messages: [...planned.value.messages, ...this.#collectAfter(selected)]
11420
- } : planned.value;
11791
+ ...selection,
11792
+ messages: [...selection.messages, ...this.#collectAfter(selected)]
11793
+ } : selection;
11421
11794
  }
11422
11795
  #refuse(error, filed, conversation) {
11796
+ if (conversation === this.#conversation && !(isLedgerError(error) && error.code === "REQUEST")) this.#current()?.file([]);
11423
11797
  return {
11424
11798
  ...filed,
11425
11799
  messages: conversation.view(),
@@ -11446,7 +11820,11 @@ var Ledger = class {
11446
11820
  topics: [...near]
11447
11821
  });
11448
11822
  const total = Math.max(0, (this.#options.capacity - this.#predict) * this.#share.prompt - (this.#gauge?.overhead ?? 0)) / (1 + LEDGER_SCALE_DRIFT);
11449
- const tail = this.#selectTail(request, total * this.#share.tail, input, scan);
11823
+ const built = this.#agent.context.build({
11824
+ messages: [request],
11825
+ judgments: []
11826
+ });
11827
+ const tail = this.#selectTail(request, total, input, scan);
11450
11828
  const tailIds = new Set(tail.map((message) => message.id));
11451
11829
  const scoped = records.some((record) => record.key !== LEDGER_RULES_KEY);
11452
11830
  const held = /* @__PURE__ */ new Set([...projection.records.filter((record) => scoped || record.key === "rules").flatMap((record) => record.members), ...records.length > 0 ? projection.records.flatMap((record) => record.members.filter((id) => this.#conversation.message(id)?.role === "user" && this.#classifier.category(id) === "rule" && this.#classifier.decisive(id))) : []]);
@@ -11494,25 +11872,121 @@ var Ledger = class {
11494
11872
  source: unit.source,
11495
11873
  record: this.#buildUnitRecord(unit.source, unit.category, input, projection, held)
11496
11874
  }));
11497
- const trimmed = this.#trim(steps, records, mapped, new Set(ordered.map((unit) => unit.source)), request, tail, total, projection.amendments, scan);
11498
- const shown = /* @__PURE__ */ new Set([...trimmed.included, ...records.flatMap((record) => record.members.filter((source) => {
11499
- const full = record.lines.filter((line) => line.source === source);
11500
- const visible = trimmed.records.find((one) => one.key === record.key)?.lines.filter((line) => line.source === source) ?? [];
11501
- return full.length > 0 && full.length === visible.length;
11502
- }))]);
11503
- return { messages: [...tail.slice(0, -1).map((message) => message.role === "tool" ? this.#renderTailStub(message, scan, shown) : message), buildRequestMessage(request, trimmed.rendered)] };
11504
- }
11505
- #trim(steps, records, mapped, included, request, tail, total, amendments, scan) {
11506
- const built = this.#agent.context.build({
11507
- messages: [request],
11508
- judgments: []
11875
+ const trimmed = this.#trim(steps, records, mapped, new Set(ordered.map((unit) => unit.source)), built, tail, total, projection.amendments, scan);
11876
+ const replies = collectLedgerReplies(scan.messages, this.#requests, this.#annotations);
11877
+ const sources = new Set(this.#window.flatMap((entry) => [
11878
+ entry.request.id,
11879
+ entry.reply.id,
11880
+ ...entry.lines.map((line) => line.source)
11881
+ ]));
11882
+ const evidence = collectLedgerEvidence(scan.messages, sources, projection.amendments);
11883
+ const prefix = this.#window.flatMap((entry) => entry.lines);
11884
+ const prior = buildLedgerRenderContext(scan.messages, prefix, projection.amendments);
11885
+ const combined = buildLedgerRenderContext(scan.messages, [...prefix, ...trimmed.rendered.lines], projection.amendments);
11886
+ const stale = evidence.messages.size !== this.#evidence.messages.size || [...evidence.messages].some((receipt) => !this.#evidence.messages.has(receipt)) || evidence.amendments.size !== this.#evidence.amendments.size || [...evidence.amendments.keys()].some((receipt) => !this.#evidence.amendments.has(receipt)) || this.#window.some((entry) => replies.get(entry.request.id)?.id !== entry.reply.id || entry.lines.some((line) => !live.has(line.source))) || projection.amendments.some((edge) => prior.first.has(edge.earlier) && prior.complete.has(edge.later) !== combined.complete.has(edge.later) || prior.first.has(edge.later) && combined.complete.has(edge.later) && !prior.first.has(edge.earlier) && combined.first.has(edge.earlier));
11887
+ const last = stale ? this.#window.at(-1) : void 0;
11888
+ const retained = last === void 0 || replies.get(last.request.id)?.id !== last.reply.id ? void 0 : this.#conversation.message(last.request.id);
11889
+ if (stale) this.#window = [];
11890
+ const [previous, reply] = this.#window.length === 0 ? this.#selectExchange(request, input) : [];
11891
+ const window = this.#window.length > 0 ? this.#window : previous !== void 0 && reply !== void 0 ? [{
11892
+ request: previous,
11893
+ reply,
11894
+ lines: []
11895
+ }] : retained !== void 0 && last !== void 0 ? [{
11896
+ request: retained,
11897
+ reply: last.reply,
11898
+ lines: []
11899
+ }] : [];
11900
+ if (this.#window.length === 0) {
11901
+ const derived = collectLedgerEvidence(scan.messages, new Set(window.flatMap((entry) => [entry.request.id, entry.reply.id])), projection.amendments);
11902
+ this.#window = window;
11903
+ this.#evidence = derived;
11904
+ }
11905
+ const carried = window.flatMap((entry) => entry.lines);
11906
+ const delta = this.#render(trimmed.records, mapped, trimmed.included, projection.amendments, scan, new Set(carried.map(buildLedgerLineKey)), carried);
11907
+ const newest = window.at(-1);
11908
+ const plain = newest === void 0 ? void 0 : this.#conversation.message(newest.request.id);
11909
+ const fallback = newest === void 0 || plain === void 0 ? [] : [{
11910
+ request: plain,
11911
+ reply: newest.reply,
11912
+ lines: []
11913
+ }];
11914
+ const empty = {
11915
+ window: [],
11916
+ rendered: trimmed.rendered
11917
+ };
11918
+ const rungs = [
11919
+ {
11920
+ window,
11921
+ rendered: delta
11922
+ },
11923
+ {
11924
+ window: fallback,
11925
+ rendered: trimmed.rendered
11926
+ },
11927
+ empty
11928
+ ];
11929
+ const probes = rungs.map((rung) => [...built.slice(0, -1), ...stripThinking([
11930
+ ...tail.slice(0, -1),
11931
+ ...rung.window.flatMap((entry) => [entry.request, entry.reply]),
11932
+ buildRequestMessage(built.at(-1) ?? request, rung.rendered.text)
11933
+ ], this.#replay)]);
11934
+ const names = this.#agent.context.scope?.tools;
11935
+ const definitions = this.#agent.context.tools.definitions();
11936
+ const offered = rungs.map((rung) => hasHiddenRecall(input, request.id, this.#requests, this.#annotations, [...tail.slice(0, -1), ...rung.window.flatMap((entry) => [entry.request, entry.reply])], [...rung.window.flatMap((entry) => entry.lines), ...rung.rendered.lines]));
11937
+ const longest = this.#findLongest();
11938
+ const closed = probes.map((messages, at) => {
11939
+ const tools = definitions.filter((tool) => (names === void 0 || names.includes(tool.name)) && (tool.name !== "recall" || offered[at]));
11940
+ const calls = [{
11941
+ estimate: estimateMessages(messages),
11942
+ tools: tools.length,
11943
+ spared: this.#spared(tools)
11944
+ }];
11945
+ return offered[at] === true && this.#gauge !== void 0 && this.#gauge.remainder(calls) - this.#predict < 2 * this.#gauge.reserve(calls, longest);
11509
11946
  });
11947
+ const prices = probes.map((messages) => this.#price(messages));
11948
+ const admitted = rungs.find((_rung, at) => (prices[at] ?? Infinity) <= total && (closed[2] === true || closed[at] === false)) ?? empty;
11949
+ if (offered[rungs.indexOf(admitted)] === false && (names === void 0 || names.includes("recall"))) {
11950
+ const tools = definitions.filter((tool) => tool.name !== "recall").map((tool) => tool.name);
11951
+ const narrowed = this.#agent.context.scope?.narrow({ tools }) ?? createScope({
11952
+ name: "ledger",
11953
+ tools
11954
+ });
11955
+ this.#current()?.apply(narrowed);
11956
+ this.#agent.context.apply(narrowed);
11957
+ }
11958
+ const copy = buildRequestMessage(request, admitted.rendered.text);
11959
+ const kept = admitted.window.flatMap((entry) => entry.lines);
11960
+ const origins = /* @__PURE__ */ new Set([
11961
+ request.id,
11962
+ ...admitted.window.flatMap((entry) => [entry.request.id, entry.reply.id]),
11963
+ ...kept.map((line) => line.source),
11964
+ ...admitted.rendered.lines.map((line) => line.source)
11965
+ ]);
11966
+ const draft = {
11967
+ copy,
11968
+ lines: admitted.rendered.lines,
11969
+ evidence: collectLedgerEvidence(scan.messages, origins, projection.amendments),
11970
+ window: admitted.window
11971
+ };
11972
+ const shown = buildLedgerRenderContext(scan.messages, [...kept, ...admitted.rendered.lines], []).complete;
11973
+ return {
11974
+ draft,
11975
+ messages: [
11976
+ ...tail.slice(0, -1).map((message) => message.role === "tool" ? this.#renderTailStub(message, scan, shown) : message),
11977
+ ...admitted.window.flatMap((entry) => [entry.request, entry.reply]),
11978
+ copy
11979
+ ]
11980
+ };
11981
+ }
11982
+ #trim(steps, records, mapped, included, built, tail, total, amendments, scan) {
11510
11983
  const head = built.slice(0, -1);
11511
- const priced = built.at(-1) ?? request;
11984
+ const priced = built.at(-1);
11985
+ if (priced === void 0) throw new Error("a built plan holds its request");
11512
11986
  const found = findLedgerCut(0, steps.length, (count) => {
11513
11987
  const cut = buildLedgerCut(steps.slice(0, count), records, included);
11514
11988
  const rendered = this.#render(cut.records, mapped, cut.included, amendments, scan);
11515
- const copy = buildRequestMessage(priced, rendered);
11989
+ const copy = buildRequestMessage(priced, rendered.text);
11516
11990
  return this.#price([...head, ...stripThinking([...tail.slice(0, -1), copy], this.#replay)]) <= total ? {
11517
11991
  ...cut,
11518
11992
  rendered
@@ -11537,36 +12011,48 @@ var Ledger = class {
11537
12011
  const live = collectProjectionIds(projection);
11538
12012
  return scanAmendments(input.classification.amendments, source, (id) => id === source || !held.has(id) && live.has(id)).flatMap((id) => this.#projectLines(input, id));
11539
12013
  }
11540
- #render(records, mapped, included, amendments, scan) {
11541
- const entries = mapped.filter((unit) => included.has(unit.source)).flatMap(({ record: unit }) => [...Map.groupBy(unit.lines, (line) => line.source)].map(([source, lines]) => ({
12014
+ #render(records, mapped, included, amendments, scan, held = /* @__PURE__ */ new Set(), prefix = []) {
12015
+ const population = records.map((record) => ({
12016
+ ...record,
12017
+ lines: collectLedgerLines(record, held)
12018
+ }));
12019
+ const entries = mapped.filter((unit) => included.has(unit.source)).flatMap(({ record: unit }) => [...Map.groupBy(collectLedgerLines(unit, /* @__PURE__ */ new Set()), (line) => line.source)].map(([source, lines]) => ({
11542
12020
  unit,
11543
12021
  source,
11544
12022
  lines
11545
12023
  })));
11546
12024
  const first = new Map(entries.toReversed().map((entry) => [entry.source, entry]));
11547
- const unique = entries.filter((entry) => first.get(entry.source) === entry);
11548
- const record = records.find((candidate) => candidate.key === "rules" && candidate.lines.length > 0);
11549
- const ownerRecords = records.filter((candidate) => candidate.key !== "rules" && candidate.lines.length > 0);
11550
- const context = buildLedgerRenderContext(this.#conversation.messages(), [
12025
+ const unique = entries.filter((entry) => first.get(entry.source) === entry).map((entry) => ({
12026
+ ...entry,
12027
+ lines: entry.lines.filter((line) => !prefix.some((prior) => prior.source === line.source && prior.sentence === line.sentence))
12028
+ })).filter((entry) => entry.lines.length > 0);
12029
+ const record = population.find((candidate) => candidate.key === "rules" && candidate.lines.length > 0);
12030
+ const ownerRecords = population.filter((candidate) => candidate.key !== "rules" && candidate.lines.length > 0);
12031
+ const lines = [
11551
12032
  ...ownerRecords.flatMap((candidate) => candidate.lines),
11552
12033
  ...unique.filter((entry) => entry.unit.key !== LEDGER_RULES_KEY).flatMap((entry) => entry.lines),
11553
12034
  ...record?.lines ?? [],
11554
12035
  ...unique.filter((entry) => entry.unit.key === LEDGER_RULES_KEY).flatMap((entry) => entry.lines)
11555
- ], amendments);
12036
+ ];
12037
+ const context = buildLedgerRenderContext(scan.messages, [...prefix, ...lines], amendments);
11556
12038
  const owners = ownerRecords.map((owner) => renderLedgerPinned(owner, context)).join("\n\n");
11557
- const rendered = unique.map(({ unit, source, lines }) => {
12039
+ const rendered = unique.map(({ unit, source, lines: retained }) => {
11558
12040
  const sentences = splitSentences(this.#conversation.message(source)?.content ?? "");
11559
12041
  return {
11560
12042
  rules: unit.key === LEDGER_RULES_KEY,
11561
- texts: unit.key === "rules" && source === unit.members[0] ? lines.map((line) => this.#renderSource(source, sentences, [line], context, scan)) : [this.#renderSource(source, sentences, lines, context, scan)]
12043
+ texts: unit.key === "rules" && source === unit.members[0] ? retained.map((line) => this.#renderSource(source, sentences, [line], context, scan)) : [this.#renderSource(source, sentences, retained, context, scan)]
11562
12044
  };
11563
12045
  });
11564
12046
  const pinned = rendered.filter((one) => !one.rules).flatMap((one) => one.texts);
11565
12047
  const rules = rendered.filter((one) => one.rules).flatMap((one) => one.texts);
11566
12048
  const body = [owners, pinned.join("\n")].filter((text) => text !== "").join("\n\n");
11567
- return [body === "" ? "" : `## Records\n${body}`, record === void 0 ? rules.length === 0 ? "" : ["## Rules", ...rules].join("\n") : [renderLedgerRecord(record, context), ...rules].join("\n")].filter((text) => text !== "").join("\n\n");
12049
+ return {
12050
+ text: [body === "" ? "" : `## Records\n${body}`, record === void 0 ? rules.length === 0 ? "" : ["## Rules", ...rules].join("\n") : [renderLedgerRecord(record, context), ...rules].join("\n")].filter((part) => part !== "").join("\n\n"),
12051
+ lines
12052
+ };
11568
12053
  }
11569
- #selectTail(request, cap, input, scan) {
12054
+ #selectTail(request, total, input, scan) {
12055
+ const cap = total * this.#share.tail;
11570
12056
  const first = input.messages.findIndex((message) => this.#requests.has(message.id));
11571
12057
  const seed = input.messages.slice(0, first < 0 ? input.messages.findIndex((message) => message.id === request.id) : first);
11572
12058
  const groups = collectToolGroups(seed).flatMap((group) => {
@@ -11597,6 +12083,29 @@ var Ledger = class {
11597
12083
  const stop = exchanges.findIndex((_exchange, index) => this.#price(stripThinking([...exchanges.slice(0, index + 1).toReversed().flat(), request], this.#replay)) > cap);
11598
12084
  return [...exchanges.slice(0, stop === -1 ? exchanges.length : stop).toReversed().flat(), request];
11599
12085
  }
12086
+ #selectExchange(request, input) {
12087
+ const before = input.messages.slice(0, input.messages.findIndex((message) => message.id === request.id));
12088
+ const previous = before.findLast((message) => this.#requests.has(message.id));
12089
+ const reply = previous === void 0 ? void 0 : collectLedgerReplies(before, this.#requests, this.#annotations).get(previous.id);
12090
+ return previous !== void 0 && reply !== void 0 ? [previous, reply] : [];
12091
+ }
12092
+ #commitExchange(request) {
12093
+ const draft = request.sent;
12094
+ const message = request.message;
12095
+ if (message === void 0) return;
12096
+ const reply = collectLedgerReplies(this.#conversation.messages(), this.#requests, this.#annotations).get(message.id);
12097
+ if (reply === void 0) return;
12098
+ this.#window = [...draft?.window ?? this.#window, {
12099
+ request: draft?.copy ?? message,
12100
+ reply,
12101
+ lines: draft?.lines ?? []
12102
+ }];
12103
+ const evidence = draft?.evidence ?? this.#evidence;
12104
+ this.#evidence = {
12105
+ messages: /* @__PURE__ */ new Set([...evidence.messages, ...collectLedgerEvidence([message, reply], /* @__PURE__ */ new Set([message.id, reply.id]), []).messages]),
12106
+ amendments: evidence.amendments
12107
+ };
12108
+ }
11600
12109
  #renderTailStub(message, scan, shown) {
11601
12110
  const group = scan.group(message.id);
11602
12111
  const call = group === void 0 ? void 0 : findLedgerCall(group, message);
@@ -11605,7 +12114,7 @@ var Ledger = class {
11605
12114
  const visible = renderStub(call?.name ?? "tool", call?.arguments ?? {}, "shown");
11606
12115
  return {
11607
12116
  ...message,
11608
- content: renderStub(call?.name ?? "tool", call?.arguments ?? {}, this.#results.get(message.id)?.success === false ? "failed" : finding?.result === void 0 ? "empty" : shown === void 0 ? hidden.length > visible.length ? "hidden" : "shown" : shown.has(message.id) ? "shown" : "hidden")
12117
+ content: renderStub(call?.name ?? "tool", call?.arguments ?? {}, this.#results.get(message.id)?.success === false ? "failed" : finding?.result === void 0 ? "empty" : shown === void 0 ? hidden.length > visible.length ? "hidden" : "shown" : scan.findings.some((other) => shown.has(other.id) && matchesLookup(finding, other)) ? "shown" : "hidden")
11609
12118
  };
11610
12119
  }
11611
12120
  #observeTurn() {
@@ -11616,11 +12125,19 @@ var Ledger = class {
11616
12125
  const additions = this.#conversation.messages().slice(boundary);
11617
12126
  const messages = this.#agent.context.build(request.selection);
11618
12127
  const names = this.#agent.context.scope?.tools;
12128
+ const tools = this.#agent.context.tools.definitions().filter((tool) => names === void 0 || names.includes(tool.name));
11619
12129
  request.observe({
11620
12130
  estimate: estimateMessages(stripThinking([...messages, ...additions], this.#replay)),
11621
- tools: this.#agent.context.tools.definitions().filter((tool) => names === void 0 || names.includes(tool.name)).length
12131
+ tools: tools.length,
12132
+ spared: this.#spared(tools)
11622
12133
  }, this.#conversation.messages().length);
11623
12134
  }
12135
+ #spared(tools) {
12136
+ const overhead = this.#gauge?.overhead ?? 0;
12137
+ if (tools.length === 0) return overhead;
12138
+ const missing = this.#agent.context.tools.definitions().filter((tool) => !tools.some((one) => one.name === tool.name));
12139
+ return missing.length === 0 ? 0 : Math.min(overhead, estimateTokens(JSON.stringify(missing)) * (this.#gauge?.scale ?? 1));
12140
+ }
11624
12141
  #findLongest() {
11625
12142
  return this.#conversation.messages().filter((message) => message.role === "assistant").reduce((longest, message) => message.content.length > longest.length ? message.content : longest, "");
11626
12143
  }
@@ -11637,11 +12154,21 @@ var Ledger = class {
11637
12154
  request?.admit("recall", { topic });
11638
12155
  const gauge = this.#gauge;
11639
12156
  const calls = request?.calls ?? [];
11640
- if (gauge !== void 0 && calls.length > 0 && gauge.remainder(calls) - this.#predict < 2 * gauge.reserve(calls, this.#findLongest())) request?.close();
11641
12157
  request?.recall();
11642
12158
  const guidance = `an owner name, an id, or one of ${renderTopicNames(this.#options.topics)}`;
11643
12159
  if (topic === "") throw new Error(`recall needs a topic: ${guidance}`);
11644
- const room = (gauge?.room(calls, this.#findLongest()) ?? 0) - estimateMessages([buildRecallMessage(topic)]);
12160
+ const rate = this.#recallRate(request) * (1 + LEDGER_SCALE_DRIFT);
12161
+ const call = calls.at(-1);
12162
+ const carried = request === void 0 ? 0 : estimateMessages(stripThinking([...this.#agent.context.build(request.selection), ...this.#conversation.messages().slice(request.boundary)], this.#replay));
12163
+ const completion = (0, _orkestrel_contract.isFiniteNumber)(call?.prompt) ? (call.completion ?? 0) - (this.#replay === "none" ? call.thinking ?? 0 : 0) : 0;
12164
+ const extra = Math.max(0, (carried - (call?.estimate ?? carried)) * rate - completion);
12165
+ const drift = (0, _orkestrel_contract.isFiniteNumber)(call?.prompt) ? 0 : Math.max(0, (call?.estimate ?? 0) * (rate - (gauge?.scale ?? rate)));
12166
+ const siblings = estimateMessages((request?.listings ?? []).filter((listing) => listing.turn === calls.length).map((listing) => ({
12167
+ id: "result",
12168
+ role: "tool",
12169
+ content: listing.result
12170
+ })));
12171
+ const room = gauge === void 0 ? -estimateMessages([buildRecallMessage(topic)]) : (gauge.remainder(calls) - this.#predict - extra - drift) / rate - siblings;
11645
12172
  const scan = this.#scan(this.#conversation.messages());
11646
12173
  const added = this.#scans.size === 0;
11647
12174
  if (added) this.#hold(scan);
@@ -11673,11 +12200,12 @@ var Ledger = class {
11673
12200
  const source = listable.get(id);
11674
12201
  if (source === void 0) return [];
11675
12202
  const lookup = scan.finding(id);
11676
- const text = lookup === void 0 ? source.content : `${renderLookupCall(lookup.name, lookup.arguments)}: ${source.content}`;
12203
+ const content = source.content.replace(/(^|[\n\v\f\r\u0085\u2028\u2029])(?=Earlier (?:request|reply):)/g, "$1 ");
12204
+ const text = lookup === void 0 ? content : `${renderLookupCall(lookup.name, lookup.arguments)}: ${content}`;
11677
12205
  return [{
11678
12206
  id,
11679
12207
  text,
11680
- stored: lookup === void 0 ? [] : [[splitResultLines(text)[0] ?? "", splitResultLines(source.content)[0] ?? ""]]
12208
+ stored: lookup === void 0 ? [] : [[splitResultLines(text)[0] ?? "", splitResultLines(content)[0] ?? ""]]
11681
12209
  }];
11682
12210
  });
11683
12211
  return {
@@ -11690,8 +12218,15 @@ var Ledger = class {
11690
12218
  listed: [],
11691
12219
  recalled: /* @__PURE__ */ new Map()
11692
12220
  });
11693
- const result = gathered.listed.length === 0 ? `nothing on "${topic}"; recall ${guidance}` : cutListing(gathered.listed, room);
11694
- request?.list(result, gathered.recalled);
12221
+ const earlier = input.messages.slice(0, Math.max(0, start)).reverse().flatMap((message) => {
12222
+ if (gathered.done.has(message.id) || this.#annotations.has(message.id)) return [];
12223
+ const label = this.#requests.has(message.id) ? LEDGER_RECALL_LABELS.request : message.role === "assistant" && !hasCalls(message) && message.content.trim() !== "" ? LEDGER_RECALL_LABELS.reply : void 0;
12224
+ if (label === void 0 || !searches.some((search) => search.words.length > 0 && search.words.every((word) => message.content.toLowerCase().includes(word)))) return [];
12225
+ return [`${label} ${message.content.replace(/[\n\v\f\r\u0085\u2028\u2029]+/g, "\n ")}`];
12226
+ });
12227
+ const listed = [...gathered.listed, ...earlier];
12228
+ const result = listed.length === 0 ? `nothing earlier on "${topic}"; answer from the messages you can see` : cutListing(listed, room, earlier.length > 0 ? "more" : "older", gauge === void 0);
12229
+ request?.list(result, gathered.recalled, listed);
11695
12230
  return result;
11696
12231
  } finally {
11697
12232
  if (added) this.#scans.delete(scan);
@@ -11699,15 +12234,58 @@ var Ledger = class {
11699
12234
  }
11700
12235
  #buildDigest(request, entered) {
11701
12236
  this.#flush();
11702
- const lines = [...new Set(this.#collectAfter(entered).flatMap((message) => {
12237
+ const groups = [...new Set(this.#collectAfter(entered).flatMap((message) => {
11703
12238
  const result = this.#results.get(message.id);
11704
12239
  if (message.role !== "tool" || result?.success !== true) return [];
11705
12240
  const recalled = result.name === "recall";
11706
- if (recalled && /^nothing on /.test(message.content)) return [];
11707
- const stored = recalled ? request.lines(message.content) : void 0;
11708
- return splitResultLines(message.content).filter((line) => !recalled || !matchesCutLine(line)).map((line) => stored?.get(line) ?? line);
12241
+ if (recalled && /^nothing earlier on /.test(message.content)) return [];
12242
+ const listing = recalled ? request.listings.findLast((one) => one.result === message.content) : void 0;
12243
+ return (listing?.groups ?? [message.content]).map((group) => splitResultLines(group).map((line) => listing?.lines.get(line) ?? line).join("\n")).filter((group) => group !== "");
11709
12244
  }))];
11710
- return lines.length === 0 ? void 0 : `${LEDGER_NOTES.results}\n${lines.join("\n")}`;
12245
+ const unique = groups.filter((group, at) => !splitResultLines(group).every((line) => groups.slice(0, at).some((earlier) => splitResultLines(earlier).includes(line))));
12246
+ const digest = unique.length === 0 ? void 0 : `${LEDGER_NOTES.results}\n${unique.join("\n")}`;
12247
+ const omitted = /* @__PURE__ */ new Set();
12248
+ const rate = this.#recallRate(request) * (1 + LEDGER_SCALE_DRIFT);
12249
+ const allowance = (this.#options.capacity - this.#predict) / rate;
12250
+ const after = this.#collectAfter(entered);
12251
+ const messages = [
12252
+ ...request.plan?.messages ?? request.selection?.messages ?? [],
12253
+ ...after,
12254
+ {
12255
+ id: "cue",
12256
+ role: "user",
12257
+ content: LEDGER_NOTES.cue
12258
+ }
12259
+ ].filter((message) => message.role !== "tool" && !hasCalls(message));
12260
+ const note = digest === void 0 ? [] : [{
12261
+ id: "results",
12262
+ role: "user",
12263
+ content: digest
12264
+ }];
12265
+ if (this.#gauge === void 0 || estimateMessages(stripThinking(this.#agent.context.build({
12266
+ messages: [...messages, ...note],
12267
+ judgments: []
12268
+ }), this.#replay)) <= allowance) return {
12269
+ digest,
12270
+ omitted
12271
+ };
12272
+ const partials = new Set(after.filter((message) => message.role === "assistant" && !hasCalls(message)).map((message) => message.id));
12273
+ const fitted = cutListing(unique, allowance - estimateMessages(stripThinking(this.#agent.context.build({
12274
+ messages: messages.filter((message) => !partials.has(message.id)),
12275
+ judgments: []
12276
+ }), this.#replay)) - estimateTokens(`${LEDGER_NOTES.results}\n`), "more", false);
12277
+ return {
12278
+ digest: fitted === "" ? void 0 : `${LEDGER_NOTES.results}\n${fitted}`,
12279
+ omitted: partials
12280
+ };
12281
+ }
12282
+ #recallRate(request) {
12283
+ const gauge = this.#gauge;
12284
+ if (gauge === void 0) return 1;
12285
+ const calls = request?.calls ?? [];
12286
+ const last = calls.findLast((call) => (0, _orkestrel_contract.isFiniteNumber)(call.prompt));
12287
+ const measured = last !== void 0 && (0, _orkestrel_contract.isFiniteNumber)(last.prompt) && last.estimate > 0 && ((last.spared ?? 0) === 0 || (last.spared ?? 0) >= gauge.overhead) ? (last.prompt - Math.max(0, gauge.overhead - (last.spared ?? 0))) / last.estimate : 0;
12288
+ return Math.max(gauge.rate(calls), measured);
11711
12289
  }
11712
12290
  };
11713
12291
  // src/core/ledgers/factories.ts
@@ -11768,6 +12346,7 @@ exports.LEDGER_CATEGORIES = LEDGER_CATEGORIES;
11768
12346
  exports.LEDGER_NOTES = LEDGER_NOTES;
11769
12347
  exports.LEDGER_OWNER_PREFIX = LEDGER_OWNER_PREFIX;
11770
12348
  exports.LEDGER_QUESTIONS = LEDGER_QUESTIONS;
12349
+ exports.LEDGER_RECALL_LABELS = LEDGER_RECALL_LABELS;
11771
12350
  exports.LEDGER_RULES_KEY = LEDGER_RULES_KEY;
11772
12351
  exports.LEDGER_SCALE_DRIFT = LEDGER_SCALE_DRIFT;
11773
12352
  exports.LEDGER_SLICE = LEDGER_SLICE;
@@ -11826,6 +12405,7 @@ exports.buildJudgeResult = buildJudgeResult;
11826
12405
  exports.buildJudgments = buildJudgments;
11827
12406
  exports.buildLedgerCut = buildLedgerCut;
11828
12407
  exports.buildLedgerIndex = buildLedgerIndex;
12408
+ exports.buildLedgerLineKey = buildLedgerLineKey;
11829
12409
  exports.buildLedgerRenderContext = buildLedgerRenderContext;
11830
12410
  exports.buildLines = buildLines;
11831
12411
  exports.buildProviderHeaders = buildProviderHeaders;
@@ -11840,9 +12420,13 @@ exports.chargeUsage = chargeUsage;
11840
12420
  exports.collectAmendments = collectAmendments;
11841
12421
  exports.collectExchanges = collectExchanges;
11842
12422
  exports.collectImageData = collectImageData;
12423
+ exports.collectLedgerEvidence = collectLedgerEvidence;
12424
+ exports.collectLedgerLines = collectLedgerLines;
12425
+ exports.collectLedgerReplies = collectLedgerReplies;
11843
12426
  exports.collectLive = collectLive;
11844
12427
  exports.collectNames = collectNames;
11845
12428
  exports.collectProjectionIds = collectProjectionIds;
12429
+ exports.collectRecallMessages = collectRecallMessages;
11846
12430
  exports.collectRegistry = collectRegistry;
11847
12431
  exports.collectToolGroups = collectToolGroups;
11848
12432
  exports.computeThinking = computeThinking;
@@ -11879,6 +12463,7 @@ exports.denyCall = denyCall;
11879
12463
  exports.describeQuestionRefusal = describeQuestionRefusal;
11880
12464
  exports.describeRelayRefusal = describeRelayRefusal;
11881
12465
  exports.describeSnapshotRefusal = describeSnapshotRefusal;
12466
+ exports.digestLedgerWindow = digestLedgerWindow;
11882
12467
  exports.estimateMessages = estimateMessages;
11883
12468
  exports.estimateRequest = estimateRequest;
11884
12469
  exports.estimateTokens = estimateTokens;
@@ -11894,6 +12479,7 @@ exports.fitSlope = fitSlope;
11894
12479
  exports.freezeJSON = freezeJSON;
11895
12480
  exports.handleAgentQueueJob = handleAgentQueueJob;
11896
12481
  exports.hasCalls = hasCalls;
12482
+ exports.hasHiddenRecall = hasHiddenRecall;
11897
12483
  exports.identifyLookup = identifyLookup;
11898
12484
  exports.inferStop = inferStop;
11899
12485
  exports.intersectKeys = intersectKeys;
@@ -11915,6 +12501,7 @@ exports.isJudgment = isJudgment;
11915
12501
  exports.isLedgerError = isLedgerError;
11916
12502
  exports.isLedgerQuestion = isLedgerQuestion;
11917
12503
  exports.isLedgerSnapshot = isLedgerSnapshot;
12504
+ exports.isLedgerWindowSnapshot = isLedgerWindowSnapshot;
11918
12505
  exports.isMemo = isMemo;
11919
12506
  exports.isMemoOrigin = isMemoOrigin;
11920
12507
  exports.isMemoSnapshot = isMemoSnapshot;
@@ -11938,11 +12525,14 @@ exports.judgmentContract = judgmentContract;
11938
12525
  exports.judgmentShape = judgmentShape;
11939
12526
  exports.ledgerSnapshotContract = ledgerSnapshotContract;
11940
12527
  exports.ledgerSnapshotShape = ledgerSnapshotShape;
12528
+ exports.ledgerWindowContract = ledgerWindowContract;
12529
+ exports.ledgerWindowShape = ledgerWindowShape;
11941
12530
  exports.linkOwners = linkOwners;
11942
12531
  exports.matchEntities = matchEntities;
11943
12532
  exports.matchesCutLine = matchesCutLine;
11944
12533
  exports.matchesJSON = matchesJSON;
11945
12534
  exports.matchesJudgment = matchesJudgment;
12535
+ exports.matchesLookup = matchesLookup;
11946
12536
  exports.measureAnswer = measureAnswer;
11947
12537
  exports.measureMessage = measureMessage;
11948
12538
  exports.measureRequest = measureRequest;