@fungi.computer/shiitake 0.2.0 → 0.3.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.
Files changed (114) hide show
  1. package/README.md +155 -34
  2. package/dist/agent-control.d.ts +77 -5
  3. package/dist/agent-control.js +271 -65
  4. package/dist/client-entry.d.ts +15 -8
  5. package/dist/client-entry.js +7 -5
  6. package/dist/client-wire.d.ts +24 -8
  7. package/dist/client-wire.js +53 -11
  8. package/dist/client.d.ts +56 -11
  9. package/dist/client.js +1 -1
  10. package/dist/cloudflare.d.ts +42 -23
  11. package/dist/cloudflare.js +76 -54
  12. package/dist/cold-sessions.d.ts +28 -0
  13. package/dist/cold-sessions.js +1 -0
  14. package/dist/delegation.d.ts +214 -0
  15. package/dist/delegation.js +319 -0
  16. package/dist/effect.d.ts +1 -1
  17. package/dist/errors.d.ts +2 -2
  18. package/dist/errors.js +2 -2
  19. package/dist/extension-contract.d.ts +10 -2
  20. package/dist/extensions.d.ts +6 -2
  21. package/dist/fleet.d.ts +43 -9
  22. package/dist/fleet.js +88 -25
  23. package/dist/hosted.js +1 -0
  24. package/dist/http-routes.d.ts +113 -29
  25. package/dist/http-routes.js +18 -1
  26. package/dist/index.d.ts +3 -2
  27. package/dist/internal/accepted-run-executor.d.ts +5 -0
  28. package/dist/internal/accepted-run-executor.js +10 -4
  29. package/dist/internal/admission-door.d.ts +3 -0
  30. package/dist/internal/admission-door.js +12 -2
  31. package/dist/internal/client-boundary.js +7 -1
  32. package/dist/internal/compaction/compaction-slot.d.ts +36 -0
  33. package/dist/internal/compaction/compaction-slot.js +41 -0
  34. package/dist/internal/compaction/compaction.d.ts +11 -8
  35. package/dist/internal/compaction/compaction.js +62 -34
  36. package/dist/internal/compaction/session-compaction.d.ts +8 -16
  37. package/dist/internal/compaction/session-compaction.js +358 -119
  38. package/dist/internal/compaction/settings.d.ts +1 -1
  39. package/dist/internal/compaction/settings.js +5 -0
  40. package/dist/internal/compaction/utils.d.ts +19 -16
  41. package/dist/internal/compaction/utils.js +30 -84
  42. package/dist/internal/contracts.d.ts +40 -13
  43. package/dist/internal/coordinator.js +28 -10
  44. package/dist/internal/effect-client.d.ts +4 -2
  45. package/dist/internal/effect-client.js +29 -0
  46. package/dist/internal/effect-contracts.d.ts +17 -4
  47. package/dist/internal/extension-runtime.d.ts +5 -0
  48. package/dist/internal/extension-runtime.js +184 -89
  49. package/dist/internal/host-meta.d.ts +7 -0
  50. package/dist/internal/model-acquisition.js +7 -1
  51. package/dist/internal/model-preference.d.ts +4 -2
  52. package/dist/internal/model-preference.js +2 -1
  53. package/dist/internal/promise-facade.js +4 -16
  54. package/dist/internal/run-failure-reason.d.ts +10 -0
  55. package/dist/internal/run-failure-reason.js +19 -0
  56. package/dist/internal/run-scope.d.ts +4 -1
  57. package/dist/internal/run-scope.js +38 -5
  58. package/dist/internal/runtime-coordinator.d.ts +2 -1
  59. package/dist/internal/runtime-coordinator.js +65 -4
  60. package/dist/internal/runtime-core.js +1 -0
  61. package/dist/internal/runtime-reconciliation.js +42 -6
  62. package/dist/internal/session-context.d.ts +1 -2
  63. package/dist/internal/session-context.js +8 -30
  64. package/dist/internal/session-status.d.ts +2 -1
  65. package/dist/internal/session-status.js +4 -1
  66. package/dist/internal/woodstock/client-projection.d.ts +6 -5
  67. package/dist/internal/woodstock/client-projection.js +79 -53
  68. package/dist/internal/woodstock/cold-sessions.d.ts +4 -0
  69. package/dist/internal/woodstock/cold-sessions.js +90 -0
  70. package/dist/internal/woodstock/extension-store.d.ts +2 -0
  71. package/dist/internal/woodstock/extension-store.js +4 -0
  72. package/dist/internal/woodstock/fleet-store.d.ts +5 -1
  73. package/dist/internal/woodstock/fleet-store.js +17 -5
  74. package/dist/internal/woodstock/index.d.ts +11 -3
  75. package/dist/internal/woodstock/index.js +33 -14
  76. package/dist/internal/woodstock/progress.d.ts +14 -1
  77. package/dist/internal/woodstock/progress.js +97 -19
  78. package/dist/internal/woodstock/run-recovery.js +6 -4
  79. package/dist/internal/woodstock/session-attention.d.ts +1 -1
  80. package/dist/internal/woodstock/session-attention.js +2 -2
  81. package/dist/internal/woodstock/session-entry.d.ts +110 -43
  82. package/dist/internal/woodstock/session-entry.js +63 -20
  83. package/dist/internal/woodstock/session-inbox.js +2 -0
  84. package/dist/internal/woodstock/session-store.d.ts +104 -24
  85. package/dist/internal/woodstock/session-store.js +937 -246
  86. package/dist/internal/woodstock/storage-admission.d.ts +8 -8
  87. package/dist/internal/woodstock/storage-admission.js +8 -34
  88. package/dist/internal/woodstock/storage-conformance.d.ts +15 -0
  89. package/dist/internal/woodstock/storage-conformance.js +489 -0
  90. package/dist/model-preference.d.ts +2 -2
  91. package/dist/model-preference.js +2 -2
  92. package/dist/node.js +1 -1
  93. package/dist/remote-client.js +8 -1
  94. package/dist/session-attention.d.ts +8 -1
  95. package/dist/session-attention.js +6 -1
  96. package/dist/testing.d.ts +1 -1
  97. package/dist/testing.js +1 -0
  98. package/docs/api/README.md +18 -16
  99. package/docs/api/agent-control.md +171 -1
  100. package/docs/api/client-entry.md +26 -10
  101. package/docs/api/client.md +153 -37
  102. package/docs/api/cloudflare.md +76 -23
  103. package/docs/api/delegation.md +485 -0
  104. package/docs/api/effect.md +20 -2
  105. package/docs/api/extensions.md +21 -9
  106. package/docs/api/fleet.md +159 -104
  107. package/docs/api/http.md +4 -1
  108. package/docs/api/index.md +116 -20
  109. package/docs/api/internal/woodstock/storage-conformance.md +81 -0
  110. package/docs/api/model-preference.md +27 -10
  111. package/docs/api/testing.md +16 -4
  112. package/package.json +26 -14
  113. package/dist/internal/browser-sessions.d.ts +0 -31
  114. package/dist/internal/browser-sessions.js +0 -173
@@ -2,9 +2,9 @@ import * as Effect from "effect/Effect";
2
2
  import * as Schema from "effect/Schema";
3
3
  import { checkHostMeta } from "./host-meta.js";
4
4
  import { ShiitakeError } from "../errors.js";
5
- import { decodeClientAcceptedCommand, decodeClientSessionId, requireDecoded, } from "./client-boundary.js";
5
+ import { decodeClientAcceptedCommand, decodeClientSessionId, decodeClientSessionCreationRequestId, requireDecoded, } from "./client-boundary.js";
6
6
  import { publishOwnerObservation } from "./observation-kernel.js";
7
- import { AdmissionOriginSchema, acceptSessionCommandOwned, readSessionCommandAdmissionByRequest, hasStoredSession, VerifiedAgentReplySchema, } from "./woodstock/index.js";
7
+ import { AdmissionOriginSchema, acceptSessionCommandOwned, readSessionCommandAdmissionByRequest, withdrawSessionRequestOwned, assertSessionRequestNotWithdrawn, hasStoredSession, VerifiedAgentReplySchema, } from "./woodstock/index.js";
8
8
  const decodeOrigin = Schema.decodeUnknownResult(AdmissionOriginSchema, {
9
9
  onExcessProperty: "error",
10
10
  });
@@ -42,6 +42,7 @@ export function openAdmissionDoor(input) {
42
42
  const admitDecoded = (request, touched) => {
43
43
  const { sessionId, origin } = request;
44
44
  const command = request.command;
45
+ assertSessionRequestNotWithdrawn(input.owner, sessionId, command.requestId);
45
46
  if (request.requireExistingSession &&
46
47
  !hasStoredSession(input.owner, sessionId))
47
48
  throw new ShiitakeError("contract", "closed", "Session target is unavailable");
@@ -95,6 +96,15 @@ export function openAdmissionDoor(input) {
95
96
  const transact = (body, boundary = "request") => run((admit) => body((request) => admit(decode(request))), boundary);
96
97
  return Object.freeze({
97
98
  transact,
99
+ withdraw: (sessionId, requestId) => Effect.try({
100
+ try: () => ({
101
+ sessionId: decodeClientSessionId(sessionId),
102
+ requestId: decodeClientSessionCreationRequestId({ requestId }),
103
+ }),
104
+ catch: (error) => error instanceof ShiitakeError
105
+ ? error
106
+ : new ShiitakeError("contract", "invalid", "Withdrawal request is invalid"),
107
+ }).pipe(Effect.flatMap((decoded) => run(() => withdrawSessionRequestOwned(input.owner, decoded.sessionId, decoded.requestId), "request"))),
98
108
  admit: (request, boundary) => Effect.try({
99
109
  try: () => decode(request),
100
110
  catch: (error) => error instanceof ShiitakeError
@@ -5,7 +5,13 @@ const encoder = new TextEncoder();
5
5
  function boundedText(maximumBytes) {
6
6
  return Schema.String.check(Schema.isMinLength(1), Schema.isMaxLength(maximumBytes), Schema.makeFilter((value) => encoder.encode(value).byteLength <= maximumBytes));
7
7
  }
8
- const boundedId = boundedText(256);
8
+ /** A lone UTF-16 surrogate; SQLite's UTF-8 text columns cannot store one. */
9
+ const loneSurrogate = /\p{Cs}/u;
10
+ /**
11
+ * Session and request ids are indexed SQLite text, so they must be well-formed
12
+ * Unicode: a lone surrogate would be stored as U+FFFD and never match again.
13
+ */
14
+ const boundedId = boundedText(256).check(Schema.makeFilter((value) => !loneSurrogate.test(value)));
9
15
  /**
10
16
  * The one prompt-text grammar. Human, mail, host and extension prompts are all
11
17
  * admitted through the accepted-command door under this bound. Text is
@@ -0,0 +1,36 @@
1
+ import * as Effect from "effect/Effect";
2
+ import type * as Fiber from "effect/Fiber";
3
+ import * as Scope from "effect/Scope";
4
+ import type { SqliteOwner } from "../contracts.js";
5
+ /**
6
+ * An owner's one compaction slot (background compaction design §5). From
7
+ * claim until release it holds the only extra decoded copy of a
8
+ * conversation the owner may have: the summary fiber while it runs, then the
9
+ * summary it hands back. Every other trigger joins it instead of starting a
10
+ * second one. Process memory only: a crash loses it, unwritten.
11
+ *
12
+ * Deliberate Shiitake difference, not Pi parity: Pi Durable's background
13
+ * compaction is a durable task that survives a Conversation abort and
14
+ * resumes after a crash, and Pi does not join triggers into one. Ours is
15
+ * scoped to the run and single-flight; that is what bounds an owner to one
16
+ * extra copy of one conversation.
17
+ */
18
+ export type CompactionSlot<A, E> = {
19
+ fiber: Fiber.Fiber<void, E> | undefined;
20
+ summary: A | undefined;
21
+ };
22
+ /** Whether a compaction already holds this owner's slot. */
23
+ export declare function slotTaken(owner: SqliteOwner): boolean;
24
+ /**
25
+ * Claim the owner's slot and fork `work` into `scope`, which interrupts it
26
+ * when the run ends. The release finalizer is registered before the fiber
27
+ * or the slot entry exist, and the entry is written in the same synchronous
28
+ * step that receives the fiber, so an interrupt anywhere leaves no entry.
29
+ * `work` hands its summary back through the slot, never through the fiber's
30
+ * exit, so releasing the slot makes the summary unreachable.
31
+ */
32
+ export declare function claimSlot<A, E>(owner: SqliteOwner, scope: Scope.Scope, work: (handBack: (summary: A) => void) => Effect.Effect<void, E>): Effect.Effect<CompactionSlot<A, E>>;
33
+ /** Take the finished summary out of the slot, which no longer reaches it. */
34
+ export declare function takeSummary<A, E>(slot: CompactionSlot<A, E>): A | undefined;
35
+ /** Drop every reference the slot holds and free it, however it settled. */
36
+ export declare function releaseSlot<A, E>(owner: SqliteOwner, slot: CompactionSlot<A, E>): void;
@@ -0,0 +1,41 @@
1
+ import * as Effect from "effect/Effect";
2
+ import * as Scope from "effect/Scope";
3
+ const slots = new WeakMap();
4
+ /** Whether a compaction already holds this owner's slot. */
5
+ export function slotTaken(owner) {
6
+ return slots.has(owner);
7
+ }
8
+ /**
9
+ * Claim the owner's slot and fork `work` into `scope`, which interrupts it
10
+ * when the run ends. The release finalizer is registered before the fiber
11
+ * or the slot entry exist, and the entry is written in the same synchronous
12
+ * step that receives the fiber, so an interrupt anywhere leaves no entry.
13
+ * `work` hands its summary back through the slot, never through the fiber's
14
+ * exit, so releasing the slot makes the summary unreachable.
15
+ */
16
+ export function claimSlot(owner, scope, work) {
17
+ const slot = { fiber: undefined, summary: undefined };
18
+ const task = work((summary) => {
19
+ slot.summary = summary;
20
+ });
21
+ return Scope.addFinalizer(scope, Effect.sync(() => {
22
+ releaseSlot(owner, slot);
23
+ })).pipe(Effect.andThen(Effect.forkIn(task, scope)), Effect.map((fiber) => {
24
+ slot.fiber = fiber;
25
+ slots.set(owner, slot);
26
+ return slot;
27
+ }));
28
+ }
29
+ /** Take the finished summary out of the slot, which no longer reaches it. */
30
+ export function takeSummary(slot) {
31
+ const summary = slot.summary;
32
+ slot.summary = undefined;
33
+ return summary;
34
+ }
35
+ /** Drop every reference the slot holds and free it, however it settled. */
36
+ export function releaseSlot(owner, slot) {
37
+ slot.fiber = undefined;
38
+ slot.summary = undefined;
39
+ if (slots.get(owner) === slot)
40
+ slots.delete(owner);
41
+ }
@@ -2,8 +2,10 @@
2
2
  * Pi v0.84.4 semantic compaction, adapted only at Woodstock's entry boundary.
3
3
  * The retained preparation/result vocabulary is intentionally visible here;
4
4
  * Shiitake supplies native Message values and its selected model stream.
5
+ * Range selection follows Pi Durable spec §8.7 (`earendil-works/pi` at
6
+ * 8efd314f) and token estimates are pi-ai's own.
5
7
  */
6
- import type { Api, Model, Usage } from "@earendil-works/pi-ai";
8
+ import type { Api, Model, Usage } from "@fungi.computer/mule/effect";
7
9
  import type { MuleStreamFunction } from "@fungi.computer/mule/effect";
8
10
  import * as Effect from "effect/Effect";
9
11
  import type { BorrowedCompactionViewV0 } from "../../extensions.js";
@@ -12,13 +14,14 @@ import { createFileOps, type CompactionSettings } from "./utils.js";
12
14
  export type CompactionSourceEntry = Readonly<{
13
15
  id: string;
14
16
  kind: "message";
15
- message: import("@earendil-works/pi-ai").Message;
17
+ message: import("@fungi.computer/mule/effect").Message;
16
18
  }> | Readonly<{
17
19
  id: string;
18
20
  kind: "compaction";
19
- timestamp: number;
20
- firstKeptEntryId: string;
21
- summary: string;
21
+ /** Pi's entry head: the first entry the summary keeps. */
22
+ head: string;
23
+ /** Pi's entry model message: the summary the model reads. */
24
+ model: import("@fungi.computer/mule/effect").UserMessage;
22
25
  usage: Usage;
23
26
  details?: Readonly<{
24
27
  readFiles: readonly string[];
@@ -27,8 +30,8 @@ export type CompactionSourceEntry = Readonly<{
27
30
  }>;
28
31
  export type CompactionPreparation = Readonly<{
29
32
  firstKeptEntryId: string;
30
- messagesToSummarize: readonly import("@earendil-works/pi-ai").Message[];
31
- turnPrefixMessages: readonly import("@earendil-works/pi-ai").Message[];
33
+ messagesToSummarize: readonly import("@fungi.computer/mule/effect").Message[];
34
+ turnPrefixMessages: readonly import("@fungi.computer/mule/effect").Message[];
32
35
  isSplitTurn: boolean;
33
36
  tokensBefore: number;
34
37
  previousSummary?: string;
@@ -51,4 +54,4 @@ export declare function prepareCompaction(entries: readonly CompactionSourceEntr
51
54
  export declare function compact(preparation: CompactionPreparation, model: Model<Api>, streamFn?: MuleStreamFunction,
52
55
  /** A manual compaction's focus, appended to the history summary prompt. */
53
56
  instructions?: string): Effect.Effect<CompactionResult, ShiitakeError>;
54
- export declare function borrowedView(preparation: CompactionPreparation, result: CompactionResult, contextRevision: string): BorrowedCompactionViewV0;
57
+ export declare function borrowedView(preparation: CompactionPreparation, result: CompactionResult, sessionId: string, contextRevision: string): BorrowedCompactionViewV0;
@@ -1,11 +1,13 @@
1
+ import { normalizeContext } from "@earendil-works/pi-ai/utils/transcript";
2
+ import { streamModel } from "@fungi.computer/mule/effect";
1
3
  // Leaf helpers, not Pi's root entry, which also evaluates its validators.
4
+ import { estimateContextTokens, estimateMessageTokens, } from "@earendil-works/pi-ai/utils/estimate";
2
5
  import { retryAssistantCall } from "@earendil-works/pi-ai/utils/retry";
3
6
  import { contentText } from "@earendil-works/pi-ai/utils/text";
4
- import { normalizeContext } from "@earendil-works/pi-ai/utils/transcript";
5
7
  import { uuidv7 } from "@earendil-works/pi-ai/utils/uuid";
6
8
  import * as Effect from "effect/Effect";
7
9
  import { ShiitakeError } from "../../errors.js";
8
- import { computeFileLists, createFileOps, extractFileOpsFromMessage, formatFileOperations, serializeConversation, SUMMARIZATION_PROMPT, SUMMARIZATION_SYSTEM_PROMPT, TURN_PREFIX_SUMMARIZATION_PROMPT, UPDATE_SUMMARIZATION_PROMPT, compactionSummaryMessage, estimateContextTokens, estimateTokens, summaryFailure, } from "./utils.js";
10
+ import { computeFileLists, createFileOps, extractFileOpsFromMessage, formatFileOperations, serializeConversation, SUMMARIZATION_PROMPT, SUMMARIZATION_SYSTEM_PROMPT, TURN_PREFIX_SUMMARIZATION_PROMPT, UPDATE_SUMMARIZATION_PROMPT, compactionSummaryText, summaryFailure, } from "./utils.js";
9
11
  /** A summary is a model call: its bounded provider message is shown. */
10
12
  function failure(message) {
11
13
  return new ShiitakeError("model", "rejected", message);
@@ -41,14 +43,11 @@ function contextMessages(entries) {
41
43
  const latestCompaction = latest;
42
44
  const keptIndex = latestCompaction === undefined
43
45
  ? 0
44
- : Math.max(0, entries.findIndex(({ id }) => id === latestCompaction.firstKeptEntryId));
46
+ : Math.max(0, entries.findIndex(({ id }) => id === latestCompaction.head));
45
47
  const messages = entries.flatMap((entry, index) => entry.kind === "message" && index >= keptIndex ? [entry.message] : []);
46
48
  if (latestCompaction === undefined)
47
49
  return messages;
48
- return [
49
- compactionSummaryMessage(latestCompaction.summary, latestCompaction.timestamp),
50
- ...messages,
51
- ];
50
+ return [latestCompaction.model, ...messages];
52
51
  }
53
52
  function extractFileOperations(messages, entries, previousIndex) {
54
53
  const fileOps = createFileOps();
@@ -63,35 +62,65 @@ function extractFileOperations(messages, entries, previousIndex) {
63
62
  extractFileOpsFromMessage(message, fileOps);
64
63
  return fileOps;
65
64
  }
66
- function isCutPoint(message) {
67
- return message.role !== "toolResult";
65
+ /**
66
+ * Whether a result for one of `calls` follows `from` before the next assistant
67
+ * message, so a user message there sits between a tool call and its result.
68
+ */
69
+ function resultFollows(entries, from, endIndex, calls) {
70
+ for (let index = from; index < endIndex; index += 1) {
71
+ const entry = entries[index];
72
+ if (entry?.kind !== "message")
73
+ continue;
74
+ if (entry.message.role === "assistant")
75
+ return false;
76
+ if (entry.message.role === "toolResult" &&
77
+ calls.has(entry.message.toolCallId))
78
+ return true;
79
+ }
80
+ return false;
81
+ }
82
+ /**
83
+ * Pi Durable §8.7 rule 1: cut candidates are user and assistant messages.
84
+ * Tool results and system messages never are, so a kept assistant message
85
+ * keeps its tool results; a user message is not one while a result for a call
86
+ * of the assistant before it still follows it.
87
+ */
88
+ function cutPoints(entries, startIndex, endIndex) {
89
+ const points = [];
90
+ let openCalls = new Set();
91
+ for (let index = startIndex; index < endIndex; index += 1) {
92
+ const entry = entries[index];
93
+ if (entry?.kind !== "message")
94
+ continue;
95
+ const message = entry.message;
96
+ if (message.role === "assistant") {
97
+ points.push(index);
98
+ openCalls = new Set(message.content.flatMap((block) => block.type === "toolCall" ? [block.id] : []));
99
+ }
100
+ else if (message.role === "user" &&
101
+ !resultFollows(entries, index + 1, endIndex, openCalls))
102
+ points.push(index);
103
+ }
104
+ return points;
68
105
  }
106
+ /**
107
+ * Pi Durable §8.7 rules 2-3: walk newest to oldest adding each message's
108
+ * estimate; where the sum reaches `keepRecentTokens`, cut at the first
109
+ * candidate at or after that entry, or the newest candidate when none
110
+ * follows. Nothing is compacted while the sum never reaches it.
111
+ */
69
112
  function selectCutIndex(entries, startIndex, endIndex, keepRecentTokens) {
70
- const points = entries.flatMap((entry, index) => entry.kind === "message" &&
71
- index >= startIndex &&
72
- index < endIndex &&
73
- isCutPoint(entry.message)
74
- ? [index]
75
- : []);
76
- const firstPoint = points[0];
77
- if (firstPoint === undefined)
78
- return undefined;
113
+ const points = cutPoints(entries, startIndex, endIndex);
79
114
  let accumulated = 0;
80
- let cut = firstPoint;
81
115
  for (let index = endIndex - 1; index >= startIndex; index -= 1) {
82
116
  const entry = entries[index];
83
117
  if (entry?.kind !== "message")
84
118
  continue;
85
- accumulated += estimateTokens(entry.message);
86
- if (accumulated >= keepRecentTokens) {
87
- // Pi 0.99.2: when trailing tool results alone exceed the budget, keep
88
- // their tool call (the last cut point), never fall back to the start.
89
- cut =
90
- points.find((candidate) => candidate >= index) ?? points.at(-1) ?? cut;
91
- break;
92
- }
119
+ accumulated += estimateMessageTokens(entry.message);
120
+ if (accumulated >= keepRecentTokens)
121
+ return points.find((candidate) => candidate >= index) ?? points.at(-1);
93
122
  }
94
- return cut;
123
+ return undefined;
95
124
  }
96
125
  function classifyCut(entries, startIndex, cut) {
97
126
  const cutEntry = entries[cut];
@@ -132,9 +161,9 @@ export function prepareCompaction(entries, settings) {
132
161
  for (let index = entries.length - 1; index >= 0; index -= 1) {
133
162
  const entry = entries[index];
134
163
  if (entry?.kind === "compaction") {
135
- previousSummary = entry.summary;
164
+ previousSummary = compactionSummaryText(entry.model);
136
165
  previousIndex = index;
137
- const kept = entries.findIndex((candidate) => candidate.id === entry.firstKeptEntryId);
166
+ const kept = entries.findIndex((candidate) => candidate.id === entry.head);
138
167
  boundaryStart = kept >= 0 ? kept : index + 1;
139
168
  break;
140
169
  }
@@ -182,9 +211,7 @@ function completeSummarization(model, context, options, streamFn) {
182
211
  cacheRetention: "none",
183
212
  sessionId: options.sessionId ?? uuidv7(),
184
213
  };
185
- return retryAssistantCall(() => streamFn
186
- ? Promise.resolve(streamFn(model, normalizeContext(context), requestOptions)).then((stream) => stream.result())
187
- : import("@earendil-works/pi-ai/compat").then(({ completeSimple }) => completeSimple(model, context, requestOptions)), RETRY_POLICY, requestOptions.signal);
214
+ return retryAssistantCall(() => Promise.resolve((streamFn ?? streamModel)(model, normalizeContext(context), requestOptions)).then((stream) => stream.result()), RETRY_POLICY, requestOptions.signal);
188
215
  }
189
216
  function buildContext(promptText) {
190
217
  return {
@@ -281,8 +308,9 @@ instructions) {
281
308
  catch: (error) => failure(error instanceof Error ? error.message : String(error)),
282
309
  });
283
310
  }
284
- export function borrowedView(preparation, result, contextRevision) {
311
+ export function borrowedView(preparation, result, sessionId, contextRevision) {
285
312
  return {
313
+ sessionId,
286
314
  messagesToSummarize: preparation.messagesToSummarize,
287
315
  turnPrefixMessages: preparation.turnPrefixMessages,
288
316
  result,
@@ -1,12 +1,15 @@
1
1
  import { type Message, type Model, type Api, type MuleRunOutcome, type MuleStreamFunction } from "@fungi.computer/mule/effect";
2
2
  import * as Effect from "effect/Effect";
3
+ import type * as Scope from "effect/Scope";
3
4
  import type { CompactionHandoffV0 } from "../../extensions.js";
4
5
  import { ShiitakeError } from "../../errors.js";
6
+ import { type CompactionResult } from "./compaction.js";
5
7
  import { type CompactionSettings } from "./utils.js";
6
8
  import { type StoredSessionRow } from "../woodstock/index.js";
7
9
  import type { SqliteOwner } from "../contracts.js";
8
10
  declare const commitFailures: {
9
11
  readonly handoff: "The compaction handoff failed; the conversation context is unchanged";
12
+ readonly handoffResult: "The compaction handoff failed: it returned an invalid result; the conversation context is unchanged";
10
13
  readonly conflict: "The Session changed during compaction; the conversation context is unchanged";
11
14
  };
12
15
  /**
@@ -15,7 +18,8 @@ declare const commitFailures: {
15
18
  */
16
19
  export declare class CompactionCommitError extends ShiitakeError {
17
20
  readonly phase: keyof typeof commitFailures;
18
- constructor(phase: keyof typeof commitFailures);
21
+ readonly usage?: CompactionResult["usage"] | undefined;
22
+ constructor(phase: keyof typeof commitFailures, usage?: CompactionResult["usage"] | undefined);
19
23
  get diagnostic(): string;
20
24
  }
21
25
  /**
@@ -39,22 +43,10 @@ export type SessionCompactionInput = Readonly<{
39
43
  run: (messages: readonly Message[], betweenTurns: BetweenTurns) => Effect.Effect<MuleRunOutcome, ShiitakeError>;
40
44
  }>;
41
45
  /**
42
- * The compaction owner's context estimate: Pi's usage-plus-trailing count,
43
- * excluding provider usage that belongs before Woodstock's latest boundary.
44
- *
45
- * A provider's reported usage measures the context it actually read, so it
46
- * is authoritative only while nothing has reshaped that context since. Once
47
- * a compaction lands, every kept message's own usage figure is stale: it
48
- * priced the pre-compaction history, not the summary that replaced it. The
49
- * one safe fallback is a fresh chars/4 estimate of every message still in
50
- * view, the same method `tokensAfterCompaction` uses when it records the
51
- * marker's own `tokensAfter` at commit time; trailing-after-usage alone
52
- * would undercount (even to zero, when the kept tail's own reply is the
53
- * newest message and nothing followed it).
46
+ * Compose the retained pre-run, between-turn and post-run compaction phases.
47
+ * A background compaction is a child fiber in the run's own scope.
54
48
  */
55
- export declare function estimateThresholdContextTokens(path: readonly StoredSessionRow[], messages: readonly Message[]): number;
56
- /** Compose the retained pre-run, between-turn and post-run compaction phases. */
57
- export declare function runWithCompaction(input: SessionCompactionInput): Effect.Effect<MuleRunOutcome, ShiitakeError>;
49
+ export declare function runWithCompaction(input: SessionCompactionInput): Effect.Effect<MuleRunOutcome, ShiitakeError, Scope.Scope>;
58
50
  /** What a manual compaction did; a failure is the Effect's error. */
59
51
  export type ManualCompactionOutcome = "compacted" | "nothing_to_compact";
60
52
  /**