@fungi.computer/shiitake 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/README.md +155 -34
  2. package/dist/agent-control.d.ts +78 -11
  3. package/dist/agent-control.js +273 -70
  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 +52 -34
  22. package/dist/fleet.js +101 -63
  23. package/dist/hosted.js +1 -0
  24. package/dist/http-routes.d.ts +106 -22
  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 +7 -4
  35. package/dist/internal/compaction/compaction.js +59 -30
  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.js +5 -0
  39. package/dist/internal/compaction/utils.d.ts +19 -16
  40. package/dist/internal/compaction/utils.js +30 -84
  41. package/dist/internal/contracts.d.ts +40 -13
  42. package/dist/internal/coordinator.js +28 -10
  43. package/dist/internal/effect-client.d.ts +4 -2
  44. package/dist/internal/effect-client.js +29 -0
  45. package/dist/internal/effect-contracts.d.ts +17 -4
  46. package/dist/internal/extension-runtime.d.ts +5 -0
  47. package/dist/internal/extension-runtime.js +184 -89
  48. package/dist/internal/host-meta.d.ts +7 -0
  49. package/dist/internal/model-acquisition.js +7 -1
  50. package/dist/internal/model-preference.d.ts +2 -0
  51. package/dist/internal/model-preference.js +2 -1
  52. package/dist/internal/promise-facade.js +4 -16
  53. package/dist/internal/run-failure-reason.d.ts +10 -0
  54. package/dist/internal/run-failure-reason.js +19 -0
  55. package/dist/internal/run-scope.d.ts +4 -1
  56. package/dist/internal/run-scope.js +38 -5
  57. package/dist/internal/runtime-coordinator.d.ts +2 -1
  58. package/dist/internal/runtime-coordinator.js +65 -4
  59. package/dist/internal/runtime-core.js +1 -0
  60. package/dist/internal/runtime-reconciliation.js +42 -6
  61. package/dist/internal/session-context.d.ts +1 -2
  62. package/dist/internal/session-context.js +8 -30
  63. package/dist/internal/session-status.d.ts +2 -1
  64. package/dist/internal/session-status.js +4 -1
  65. package/dist/internal/woodstock/client-projection.d.ts +6 -5
  66. package/dist/internal/woodstock/client-projection.js +79 -53
  67. package/dist/internal/woodstock/cold-sessions.d.ts +4 -0
  68. package/dist/internal/woodstock/cold-sessions.js +90 -0
  69. package/dist/internal/woodstock/extension-store.d.ts +2 -0
  70. package/dist/internal/woodstock/extension-store.js +4 -0
  71. package/dist/internal/woodstock/fleet-store.d.ts +3 -3
  72. package/dist/internal/woodstock/fleet-store.js +16 -11
  73. package/dist/internal/woodstock/index.d.ts +11 -3
  74. package/dist/internal/woodstock/index.js +33 -14
  75. package/dist/internal/woodstock/progress.d.ts +14 -1
  76. package/dist/internal/woodstock/progress.js +97 -19
  77. package/dist/internal/woodstock/run-recovery.js +6 -4
  78. package/dist/internal/woodstock/session-attention.d.ts +1 -1
  79. package/dist/internal/woodstock/session-attention.js +2 -2
  80. package/dist/internal/woodstock/session-entry.d.ts +109 -42
  81. package/dist/internal/woodstock/session-entry.js +63 -20
  82. package/dist/internal/woodstock/session-inbox.js +2 -0
  83. package/dist/internal/woodstock/session-store.d.ts +104 -24
  84. package/dist/internal/woodstock/session-store.js +937 -246
  85. package/dist/internal/woodstock/storage-admission.d.ts +8 -8
  86. package/dist/internal/woodstock/storage-admission.js +8 -34
  87. package/dist/internal/woodstock/storage-conformance.d.ts +15 -0
  88. package/dist/internal/woodstock/storage-conformance.js +489 -0
  89. package/dist/model-preference.d.ts +2 -2
  90. package/dist/model-preference.js +2 -2
  91. package/dist/node.js +1 -1
  92. package/dist/remote-client.js +8 -1
  93. package/dist/session-attention.d.ts +8 -1
  94. package/dist/session-attention.js +6 -1
  95. package/dist/testing.js +1 -0
  96. package/docs/api/README.md +18 -16
  97. package/docs/api/agent-control.md +172 -2
  98. package/docs/api/client-entry.md +26 -10
  99. package/docs/api/client.md +153 -37
  100. package/docs/api/cloudflare.md +76 -23
  101. package/docs/api/delegation.md +485 -0
  102. package/docs/api/effect.md +20 -2
  103. package/docs/api/extensions.md +21 -9
  104. package/docs/api/fleet.md +149 -122
  105. package/docs/api/http.md +4 -1
  106. package/docs/api/index.md +116 -20
  107. package/docs/api/internal/woodstock/storage-conformance.md +81 -0
  108. package/docs/api/model-preference.md +17 -0
  109. package/docs/api/testing.md +16 -4
  110. package/package.json +26 -14
  111. package/dist/internal/browser-sessions.d.ts +0 -31
  112. package/dist/internal/browser-sessions.js +0 -173
@@ -7,10 +7,11 @@ import * as Schema from "effect/Schema";
7
7
  import { ShiitakeError } from "../errors.js";
8
8
  import { ExtensionId } from "../extension-contract.js";
9
9
  import { publishOwnerObservation } from "./observation-kernel.js";
10
+ import { returnedFailureReason } from "./run-failure-reason.js";
10
11
  import { coreProtocolDigest } from "./extension-roster.js";
11
12
  import { runAcceptedCommand, runAcceptedCompaction, ToolSetupError, } from "./run-scope.js";
12
13
  import { CompactionCommitError } from "./compaction/session-compaction.js";
13
- import { isCompactCommand, loadAcceptedRun, readExtensionAdmissionOrigin, recordWorkSettlement, } from "./woodstock/index.js";
14
+ import { BrokenSessionError, isCompactCommand, loadAcceptedRun, readExtensionAdmissionOrigin, recordWorkSettlement, } from "./woodstock/index.js";
14
15
  /** Bounded process-loss recovery for every accepted Agent turn. */
15
16
  export const agentTurnRecovery = Object.freeze({ maxRecoveries: 2 });
16
17
  function runtimeFailure(message) {
@@ -23,7 +24,9 @@ function runtimeFailure(message) {
23
24
  */
24
25
  function failedDecision(error) {
25
26
  const category = `${error.boundary}:${error.cause}`;
26
- const diagnostic = error instanceof ToolSetupError || error instanceof CompactionCommitError
27
+ const diagnostic = error instanceof ToolSetupError ||
28
+ error instanceof CompactionCommitError ||
29
+ error instanceof BrokenSessionError
27
30
  ? error.diagnostic
28
31
  : error.boundary === "model"
29
32
  ? error.message
@@ -114,7 +117,7 @@ export function executeAcceptedRun(options, coreDigest, job, extensions) {
114
117
  : yield* coreProtocolDigest(fileTools.map((tool) => tool.descriptor.name)),
115
118
  extensions: extensions?.runParticipants() ?? [],
116
119
  };
117
- const run = yield* runAcceptedCommand(options, prepared, executionContract, extensionModules, fileTools, extensionHandoff, extensions === undefined
120
+ const { run, failureReason } = yield* runAcceptedCommand(options, prepared, executionContract, extensionModules, fileTools, extensionHandoff, extensions === undefined
118
121
  ? undefined
119
122
  : () => extensions.promptSections(runIdentity, participants));
120
123
  switch (run.status) {
@@ -123,7 +126,10 @@ export function executeAcceptedRun(options, coreDigest, job, extensions) {
123
126
  case "cancelled":
124
127
  return { kind: "cancelled" };
125
128
  case "failed":
126
- return { kind: "failed" };
129
+ return {
130
+ kind: "failed",
131
+ reason: failureReason ?? returnedFailureReason(run),
132
+ };
127
133
  }
128
134
  }).pipe(Effect.catch((error) => logRunFailed(job.jobId, error).pipe(Effect.as(failedDecision(error)))));
129
135
  }
@@ -1,6 +1,7 @@
1
1
  import type { BirdDogMail } from "@fungi.computer/bird-dog";
2
2
  import * as Effect from "effect/Effect";
3
3
  import type { AcceptedWork } from "./client-boundary.js";
4
+ import type { WithdrawResult } from "../client.js";
4
5
  import type { Json } from "../extensions.js";
5
6
  import { type HostMetaOptions } from "./host-meta.js";
6
7
  import { ShiitakeError } from "../errors.js";
@@ -50,6 +51,8 @@ export type AdmitWithin = (request: AdmitRequest) => AdmitResult;
50
51
  export type AdmissionBoundary = "request" | "reconcile";
51
52
  /** The one accepted-command door. Nothing else writes accepted commands. */
52
53
  export type AdmissionDoor = Readonly<{
54
+ /** In the same owner transaction as admission, keep the receipt or fence the key. */
55
+ withdraw: (sessionId: string, requestId: string) => Effect.Effect<WithdrawResult, ShiitakeError>;
53
56
  /** Admit one prompt in its own transaction. */
54
57
  admit: (request: AdmitRequest, boundary?: AdmissionBoundary) => Effect.Effect<AdmitResult, ShiitakeError>;
55
58
  /**
@@ -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,6 +2,8 @@
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
8
  import type { Api, Model, Usage } from "@earendil-works/pi-ai";
7
9
  import type { MuleStreamFunction } from "@fungi.computer/mule/effect";
@@ -16,9 +18,10 @@ export type CompactionSourceEntry = Readonly<{
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("@earendil-works/pi-ai").UserMessage;
22
25
  usage: Usage;
23
26
  details?: Readonly<{
24
27
  readFiles: readonly 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,12 @@
1
1
  // Leaf helpers, not Pi's root entry, which also evaluates its validators.
2
+ import { estimateContextTokens, estimateMessageTokens, } from "@earendil-works/pi-ai/utils/estimate";
2
3
  import { retryAssistantCall } from "@earendil-works/pi-ai/utils/retry";
3
4
  import { contentText } from "@earendil-works/pi-ai/utils/text";
4
5
  import { normalizeContext } from "@earendil-works/pi-ai/utils/transcript";
5
6
  import { uuidv7 } from "@earendil-works/pi-ai/utils/uuid";
6
7
  import * as Effect from "effect/Effect";
7
8
  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";
9
+ import { computeFileLists, createFileOps, extractFileOpsFromMessage, formatFileOperations, serializeConversation, SUMMARIZATION_PROMPT, SUMMARIZATION_SYSTEM_PROMPT, TURN_PREFIX_SUMMARIZATION_PROMPT, UPDATE_SUMMARIZATION_PROMPT, compactionSummaryText, summaryFailure, } from "./utils.js";
9
10
  /** A summary is a model call: its bounded provider message is shown. */
10
11
  function failure(message) {
11
12
  return new ShiitakeError("model", "rejected", message);
@@ -41,14 +42,11 @@ function contextMessages(entries) {
41
42
  const latestCompaction = latest;
42
43
  const keptIndex = latestCompaction === undefined
43
44
  ? 0
44
- : Math.max(0, entries.findIndex(({ id }) => id === latestCompaction.firstKeptEntryId));
45
+ : Math.max(0, entries.findIndex(({ id }) => id === latestCompaction.head));
45
46
  const messages = entries.flatMap((entry, index) => entry.kind === "message" && index >= keptIndex ? [entry.message] : []);
46
47
  if (latestCompaction === undefined)
47
48
  return messages;
48
- return [
49
- compactionSummaryMessage(latestCompaction.summary, latestCompaction.timestamp),
50
- ...messages,
51
- ];
49
+ return [latestCompaction.model, ...messages];
52
50
  }
53
51
  function extractFileOperations(messages, entries, previousIndex) {
54
52
  const fileOps = createFileOps();
@@ -63,35 +61,65 @@ function extractFileOperations(messages, entries, previousIndex) {
63
61
  extractFileOpsFromMessage(message, fileOps);
64
62
  return fileOps;
65
63
  }
66
- function isCutPoint(message) {
67
- return message.role !== "toolResult";
64
+ /**
65
+ * Whether a result for one of `calls` follows `from` before the next assistant
66
+ * message, so a user message there sits between a tool call and its result.
67
+ */
68
+ function resultFollows(entries, from, endIndex, calls) {
69
+ for (let index = from; index < endIndex; index += 1) {
70
+ const entry = entries[index];
71
+ if (entry?.kind !== "message")
72
+ continue;
73
+ if (entry.message.role === "assistant")
74
+ return false;
75
+ if (entry.message.role === "toolResult" &&
76
+ calls.has(entry.message.toolCallId))
77
+ return true;
78
+ }
79
+ return false;
80
+ }
81
+ /**
82
+ * Pi Durable §8.7 rule 1: cut candidates are user and assistant messages.
83
+ * Tool results and system messages never are, so a kept assistant message
84
+ * keeps its tool results; a user message is not one while a result for a call
85
+ * of the assistant before it still follows it.
86
+ */
87
+ function cutPoints(entries, startIndex, endIndex) {
88
+ const points = [];
89
+ let openCalls = new Set();
90
+ for (let index = startIndex; index < endIndex; index += 1) {
91
+ const entry = entries[index];
92
+ if (entry?.kind !== "message")
93
+ continue;
94
+ const message = entry.message;
95
+ if (message.role === "assistant") {
96
+ points.push(index);
97
+ openCalls = new Set(message.content.flatMap((block) => block.type === "toolCall" ? [block.id] : []));
98
+ }
99
+ else if (message.role === "user" &&
100
+ !resultFollows(entries, index + 1, endIndex, openCalls))
101
+ points.push(index);
102
+ }
103
+ return points;
68
104
  }
105
+ /**
106
+ * Pi Durable §8.7 rules 2-3: walk newest to oldest adding each message's
107
+ * estimate; where the sum reaches `keepRecentTokens`, cut at the first
108
+ * candidate at or after that entry, or the newest candidate when none
109
+ * follows. Nothing is compacted while the sum never reaches it.
110
+ */
69
111
  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;
112
+ const points = cutPoints(entries, startIndex, endIndex);
79
113
  let accumulated = 0;
80
- let cut = firstPoint;
81
114
  for (let index = endIndex - 1; index >= startIndex; index -= 1) {
82
115
  const entry = entries[index];
83
116
  if (entry?.kind !== "message")
84
117
  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
- }
118
+ accumulated += estimateMessageTokens(entry.message);
119
+ if (accumulated >= keepRecentTokens)
120
+ return points.find((candidate) => candidate >= index) ?? points.at(-1);
93
121
  }
94
- return cut;
122
+ return undefined;
95
123
  }
96
124
  function classifyCut(entries, startIndex, cut) {
97
125
  const cutEntry = entries[cut];
@@ -132,9 +160,9 @@ export function prepareCompaction(entries, settings) {
132
160
  for (let index = entries.length - 1; index >= 0; index -= 1) {
133
161
  const entry = entries[index];
134
162
  if (entry?.kind === "compaction") {
135
- previousSummary = entry.summary;
163
+ previousSummary = compactionSummaryText(entry.model);
136
164
  previousIndex = index;
137
- const kept = entries.findIndex((candidate) => candidate.id === entry.firstKeptEntryId);
165
+ const kept = entries.findIndex((candidate) => candidate.id === entry.head);
138
166
  boundaryStart = kept >= 0 ? kept : index + 1;
139
167
  break;
140
168
  }
@@ -281,8 +309,9 @@ instructions) {
281
309
  catch: (error) => failure(error instanceof Error ? error.message : String(error)),
282
310
  });
283
311
  }
284
- export function borrowedView(preparation, result, contextRevision) {
312
+ export function borrowedView(preparation, result, sessionId, contextRevision) {
285
313
  return {
314
+ sessionId,
286
315
  messagesToSummarize: preparation.messagesToSummarize,
287
316
  turnPrefixMessages: preparation.turnPrefixMessages,
288
317
  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
  /**