Skip to content
Source
Sign inSign up

ContextHandoffService.ts

622 lines23.9 KB

1import {
2 type OrchestrationV2AppThread,
3 OrchestrationV2ContextHandoff,
4 type OrchestrationV2TurnItem,
5 type OrchestrationV2Run,
6 type OrchestrationV2ThreadProjection,
7 ProviderInstanceId,
8 ProviderThreadId,
9 RunId,
10 ThreadId,
11} from "@t3tools/contracts";
12import * as Context from "effect/Context";
13import * as DateTime from "effect/DateTime";
14import * as Effect from "effect/Effect";
15import * as Layer from "effect/Layer";
16import * as Schema from "effect/Schema";
17​
18import {
19 DEFAULT_HANDOFF_TOKEN_CAP,
20 handoffTokenCapConfig,
21 handoffCoverage,
22 historicalMessage,
23 renderHistory,
24 selectHistory,
25} from "@t3tools/provider-core/server/handoffBudget";
26import * as IdAllocator from "@t3tools/provider-core/server/IdAllocator";
27import { createOrchestrationV2TurnItemVisibility } from "@t3tools/shared/orchestrationV2Timeline";
28import type { ProjectionStoreV2Shape } from "./ProjectionStore.ts";
29​
30export class ContextHandoffPrepareError extends Schema.TaggedError<ContextHandoffPrepareError>()(
31 "ContextHandoffPrepareError",
32 {
33 threadId: ThreadId,
34 targetRunId: RunId,
35 fromProviderThreadIds: Schema.Array(ProviderThreadId),
36 toProviderThreadId: ProviderThreadId,
37 cause: Schema.optional(Schema.Defect()),
38 },
39) {
40 override get message(): string {
41 return `Failed to prepare context handoff for run ${this.targetRunId} in thread ${this.threadId}.`;
42 }
44​
45export const ContextHandoffServiceV2Error = Schema.Union([ContextHandoffPrepareError]);
46export type ContextHandoffServiceV2Error = typeof ContextHandoffServiceV2Error.Type;
47​
48export interface ContextHandoffServiceV2Shape {
49 readonly prepareLegacyImport: (input: {
50 readonly threadId: ThreadId;
51 readonly targetRunId: RunId;
52 readonly toProviderThreadId: ProviderThreadId;
53 readonly toProviderInstanceId: ProviderInstanceId;
54 readonly items: ReadonlyArray<OrchestrationV2TurnItem>;
55 readonly createdAt: DateTime.Utc;
56 }) => Effect.Effect<OrchestrationV2ContextHandoff, ContextHandoffServiceV2Error>;
57 readonly prepareForkDelta: (input: {
58 readonly sourceThreadId: ThreadId;
59 readonly targetThreadId: ThreadId;
60 readonly targetRunId: RunId;
61 readonly transferId: OrchestrationV2ContextHandoff["transferId"];
62 readonly fromProviderThreadIds: ReadonlyArray<ProviderThreadId>;
63 readonly toProviderThreadId: ProviderThreadId;
64 readonly fromProviderInstanceId: ProviderInstanceId;
65 readonly toProviderInstanceId: ProviderInstanceId;
66 readonly coveredRunOrdinals: OrchestrationV2ContextHandoff["coveredRunOrdinals"];
67 readonly deltaItems: ReadonlyArray<OrchestrationV2TurnItem>;
68 readonly createdAt: DateTime.Utc;
69 }) => Effect.Effect<OrchestrationV2ContextHandoff, ContextHandoffServiceV2Error>;
70 readonly prepareProviderHandoff: (input: {
71 readonly threadId: ThreadId;
72 /** Thread the items came from, when it is not `threadId` (a fork's source). */
73 readonly sourceThreadId?: ThreadId;
74 readonly targetRunId: RunId;
75 readonly transferId: OrchestrationV2ContextHandoff["transferId"];
76 readonly fromProviderThreadIds: ReadonlyArray<ProviderThreadId>;
77 readonly toProviderThreadId: ProviderThreadId;
78 readonly fromProviderInstanceId: ProviderInstanceId;
79 readonly toProviderInstanceId: ProviderInstanceId;
80 readonly coveredRunOrdinals: OrchestrationV2ContextHandoff["coveredRunOrdinals"];
81 readonly runs?: ReadonlyArray<OrchestrationV2Run>;
82 readonly strategy: Extract<
83 OrchestrationV2ContextHandoff["strategy"],
84 "delta_since_target_last_seen" | "full_thread_summary"
85 >;
86 readonly items: ReadonlyArray<OrchestrationV2TurnItem>;
87 readonly createdAt: DateTime.Utc;
88 }) => Effect.Effect<OrchestrationV2ContextHandoff, ContextHandoffServiceV2Error>;
89 /** Parent activity after the fork point, prepended to a side chat's first provider turn. */
90 readonly prepareSideChatContext: (input: {
91 readonly threadId: ThreadId;
92 readonly parentThreadId: ThreadId;
93 readonly targetRunId: RunId;
94 readonly transferId: NonNullable<OrchestrationV2ContextHandoff["transferId"]>;
95 readonly toProviderThreadId: ProviderThreadId;
96 readonly fromProviderInstanceId: ProviderInstanceId;
97 readonly toProviderInstanceId: ProviderInstanceId;
98 readonly coveredRunOrdinals: OrchestrationV2ContextHandoff["coveredRunOrdinals"];
99 readonly parentItems: ReadonlyArray<OrchestrationV2TurnItem>;
100 readonly createdAt: DateTime.Utc;
101 }) => Effect.Effect<OrchestrationV2ContextHandoff, ContextHandoffServiceV2Error>;
103​
104export class ContextHandoffServiceV2 extends Context.Service<
105 ContextHandoffServiceV2,
106 ContextHandoffServiceV2Shape
107>()("t3/orchestration-v2/ContextHandoffService/ContextHandoffServiceV2") {}
108​
109function compactText(text: string, maxLength = 240): string {
110 const compacted = text.replace(/\s+/g, " ").trim();
111 if (compacted.length <= maxLength) {
112 return compacted;
113 }
114 const end = maxLength - 3;
115 const last = compacted.charCodeAt(end - 1);
116 return `${compacted.slice(0, last >= 0xd800 && last <= 0xdbff ? end - 1 : end)}...`;
118​
119export function visibleDeltaRunOrdinals(
120 projection: { readonly runs: ReadonlyArray<Pick<OrchestrationV2Run, "id" | "ordinal">> },
121 items: ReadonlyArray<Pick<OrchestrationV2TurnItem, "runId">>,
122): OrchestrationV2ContextHandoff["coveredRunOrdinals"] {
123 const ordinalByRunId = new Map(projection.runs.map((run) => [run.id, run.ordinal]));
124 let from = Infinity;
125 let to = -Infinity;
126 for (const item of items) {
127 const ordinal = item.runId === null ? undefined : ordinalByRunId.get(item.runId);
128 if (ordinal === undefined) continue;
129 from = Math.min(from, ordinal);
130 to = Math.max(to, ordinal);
131 }
132 return from === Infinity ? { from: 1, to: 1 } : { from, to };
134​
135/** Drops rolled-back runs and cancelled queued messages, which are not part of the conversation. */
136export function visibleConversationItems(
137 projection: Pick<OrchestrationV2ThreadProjection, "runs" | "attempts">,
138 items: ReadonlyArray<OrchestrationV2TurnItem>,
139): ReadonlyArray<OrchestrationV2TurnItem> {
140 const visible = createOrchestrationV2TurnItemVisibility({
141 runs: projection.runs,
142 attempts: projection.attempts,
143 items,
144 });
145 return items.filter(visible);
147​
148type ForkHistorySource = Pick<OrchestrationV2ThreadProjection, "thread" | "runs" | "attempts">;
149​
150/** Visible rows of `source`'s own runs through `throughRun`, plus its v1 imported rows. */
151const readOwnHistoryItems = Effect.fn("orchestrationV2.contextHandoff.readOwnHistoryItems")(
152 function* (
153 projectionStore: Pick<ProjectionStoreV2Shape, "getThreadRecords">,
154 source: ForkHistorySource,
155 throughRun: Pick<OrchestrationV2Run, "ordinal">,
156 ) {
157 const { turnItems } = yield* projectionStore.getThreadRecords(source.thread.id, ["turnItems"], {
158 turnItemRunIds: [
159 ...source.runs.filter((run) => run.ordinal <= throughRun.ordinal).map((run) => run.id),
160 ...(source.thread.historyOrigin === "v1_import" ? [null] : []),
161 ],
162 });
163 return visibleConversationItems(source, turnItems);
164 },
165);
166​
167/** History `thread` shows from its fork ancestors, oldest first. Side chats inherit no rows. */
168export const readInheritedHandoffItems = Effect.fn(
169 "orchestrationV2.contextHandoff.readInheritedHandoffItems",
170)(function* (
171 projectionStore: Pick<ProjectionStoreV2Shape, "getThreadRecords">,
172 thread: OrchestrationV2AppThread,
173) {
174 const segments: Array<ReadonlyArray<OrchestrationV2TurnItem>> = [];
175 const seen = new Set([thread.id]);
176 let current = thread;
177 while (
178 current.forkedFrom?.type === "run" &&
179 current.lineage.sideChat !== true &&
180 !seen.has(current.forkedFrom.threadId)
181 ) {
182 const { threadId, runId } = current.forkedFrom;
183 seen.add(threadId);
184 const source = yield* projectionStore
185 .getThreadRecords(threadId, ["runs", "attempts"])
186 .pipe(Effect.catchTags({ ProjectionStoreThreadNotFoundError: () => Effect.succeed(null) }));
187 const forkRun = source?.runs.find((run) => run.id === runId);
188 if (source === null || forkRun === undefined) break;
189 segments.unshift(yield* readOwnHistoryItems(projectionStore, source, forkRun));
190 current = source.thread;
191 }
192 return segments.flat();
193});
194​
195/** What a portable fork from `throughRun` of `source` carries: inherited rows, then its own. */
196export const readForkHistoryItems = Effect.fn(
197 "orchestrationV2.contextHandoff.readForkHistoryItems",
198)(function* (
199 projectionStore: Pick<ProjectionStoreV2Shape, "getThreadRecords">,
200 source: ForkHistorySource,
201 throughRun: Pick<OrchestrationV2Run, "ordinal">,
202) {
203 return [
204 ...(yield* readInheritedHandoffItems(projectionStore, source.thread)),
205 ...(yield* readOwnHistoryItems(projectionStore, source, throughRun)),
206 ];
207});
208​
209function summarizeDeltaItem(item: OrchestrationV2TurnItem): string | null {
210 switch (item.type) {
211 case "user_message":
212 return `- User: ${compactText(item.text)}`;
213 case "assistant_message":
214 return `- Assistant: ${compactText(item.text)}`;
215 case "command_execution":
216 return `- Command: ${compactText(item.input)}`;
217 case "file_change":
218 return `- File change: ${compactText(item.changes?.map((change) => change.path).join(", ") ?? item.fileName)}`;
219 case "checkpoint":
220 return `- Checkpoint: ${item.files.length} files`;
221 case "handoff":
222 return `- Handoff: ${compactText(item.summary ?? item.strategy)}`;
223 default:
224 return null;
225 }
227​
228function makeForkDeltaSummary(input: {
229 readonly sourceThreadId: ThreadId;
230 readonly targetThreadId: ThreadId;
231 readonly coveredRunOrdinals: OrchestrationV2ContextHandoff["coveredRunOrdinals"];
232 readonly deltaItems: ReadonlyArray<OrchestrationV2TurnItem>;
233}): string {
234 const itemLines = input.deltaItems.flatMap((item) => {
235 const line = summarizeDeltaItem(item);
236 return line === null ? [] : [line];
237 });
238 return [
239 "Merge-back context from forked conversation.",
240 `Source thread: ${input.sourceThreadId}`,
241 `Target thread: ${input.targetThreadId}`,
242 `Covered fork runs: ${input.coveredRunOrdinals.from}-${input.coveredRunOrdinals.to}`,
243 "",
244 "Fork delta:",
245 ...(itemLines.length === 0 ? ["- No user-visible delta items."] : itemLines),
246 ].join("\n");
248​
249const SIDE_CHAT_CONTEXT_MAX_CHARS = 8_000;
250​
251/** Oldest first; raw parent items are summarized only until the context cap is reached. */
252export function makeSideChatContext(input: {
253 readonly parentThreadId: ThreadId;
254 readonly digest: ReadonlyArray<string | OrchestrationV2TurnItem>;
255}): string {
256 const framing =
257 "You are in a side chat beside the main thread. The main thread may still be running in the same worktree. Answer the user's questions; do not continue or redo the main thread's task.";
258 const header = "Main thread activity since this side chat's starting point:";
259 const truncated = "- Earlier items truncated.";
260 const footer = `Use t3_thread_read on thread ${input.parentThreadId} to see the main thread's live state.`;
261 const kept: Array<string> = [];
262 let omitted = false;
263 // The cap covers the whole block, so the fixed lines are reserved before any digest line.
264 let remaining =
265 SIDE_CHAT_CONTEXT_MAX_CHARS - [framing, "", header, truncated, "", footer].join("\n").length;
266 for (let index = input.digest.length - 1; index >= 0; index--) {
267 const item = input.digest[index]!;
268 const line = typeof item === "string" ? item : summarizeDeltaItem(item);
269 if (line === null) continue;
270 if (line.length + 1 > remaining) {
271 omitted = true;
272 break;
273 }
274 kept.unshift(line);
275 remaining -= line.length + 1;
276 }
277 return [
278 framing,
279 ...(kept.length === 0 ? [] : ["", header, ...(omitted ? [truncated] : []), ...kept]),
280 "",
281 footer,
282 ].join("\n");
284​
285/** Side chat context is prepended to the provider prompt, never replayed as budgeted history. */
286export const isSideChatContextHandoff = (handoff: OrchestrationV2ContextHandoff): boolean =>
287 handoff.strategy === "manual_context" && handoff.transferId != null;
288​
289function makeLegacyImportSummary(items: ReadonlyArray<OrchestrationV2TurnItem>): string {
290 const sections = items.flatMap((item) => {
291 switch (item.type) {
292 case "user_message":
293 return [{ label: "User", body: item.text }];
294 case "assistant_message":
295 return [{ label: "Assistant", body: item.text }];
296 default:
297 return [];
298 }
299 });
300 const header =
301 "Imported conversation history from the previous T3 Code orchestrator. Use it as context; do not repeat it unless the user asks.";
302 const maxChars = 32_000;
303 const selected: Array<string> = [];
304 let remaining = maxChars - header.length - 2;
305 for (const section of sections.toReversed()) {
306 if (remaining <= 0) break;
307 const fullSection = `${section.label}:\n${section.body}`;
308 if (fullSection.length <= remaining) {
309 selected.unshift(fullSection);
310 remaining -= fullSection.length + 2;
311 continue;
312 }
313​
314 const prefix = `${section.label}:\n... `;
315 const bodyBudget = remaining - prefix.length;
316 if (bodyBudget <= 0) continue;
317​
318 const initialStart = section.body.length - bodyBudget;
319 let bodyStart = Math.max(0, initialStart);
320 if (
321 bodyStart > 0 &&
322 !/\s/.test(section.body[bodyStart - 1] ?? "") &&
323 !/\s/.test(section.body[bodyStart] ?? "")
324 ) {
325 const nextWhitespace = section.body.slice(bodyStart).search(/\s/);
326 if (nextWhitespace !== -1) {
327 bodyStart += nextWhitespace;
328 }
329 }
330 const codeUnitAtStart = section.body.charCodeAt(bodyStart);
331 const codeUnitBeforeStart = section.body.charCodeAt(bodyStart - 1);
332 if (
333 codeUnitAtStart >= 0xdc00 &&
334 codeUnitAtStart <= 0xdfff &&
335 codeUnitBeforeStart >= 0xd800 &&
336 codeUnitBeforeStart <= 0xdbff
337 ) {
338 bodyStart += 1;
339 }
340 const bodySuffix = section.body.slice(bodyStart).trimStart();
341 if (bodySuffix.length === 0) continue;
342​
343 const suffix = `${prefix}${bodySuffix}`;
344 selected.unshift(suffix);
345 remaining -= suffix.length + 2;
346 }
347 return [header, ...selected].join("\n\n");
349​
350const makeContextHandoffService = Effect.fn("orchestrationV2.ContextHandoffService.layer")(
351 function* () {
352 const idAllocator = yield* IdAllocator.IdAllocatorV2;
353 const tokenCap = yield* handoffTokenCapConfig.pipe(
354 Effect.orElseSucceed(() => DEFAULT_HANDOFF_TOKEN_CAP),
355 );
356​
357 const prepareLegacyImport = Effect.fn("orchestrationV2.contextHandoff.prepareLegacyImport")(
358 function* (input: {
359 readonly threadId: ThreadId;
360 readonly targetRunId: RunId;
361 readonly toProviderThreadId: ProviderThreadId;
362 readonly toProviderInstanceId: ProviderInstanceId;
363 readonly items: ReadonlyArray<OrchestrationV2TurnItem>;
364 readonly createdAt: DateTime.Utc;
365 }) {
366 const handoffId = yield* idAllocator.allocate
367 .contextHandoff({
368 threadId: input.threadId,
369 fromProviderInstanceId: ProviderInstanceId.make("legacy"),
370 toProviderInstanceId: input.toProviderInstanceId,
371 })
372 .pipe(
373 Effect.mapError(
374 (cause) =>
375 new ContextHandoffPrepareError({
376 threadId: input.threadId,
377 targetRunId: input.targetRunId,
378 fromProviderThreadIds: [],
379 toProviderThreadId: input.toProviderThreadId,
380 cause,
381 }),
382 ),
383 );
384 const coverage = handoffCoverage({ ...input, coveredRunOrdinals: { from: 1, to: 1 } });
385 const selected = selectHistory({
386 messages: input.items.flatMap((item) => {
387 const message = historicalMessage(item);
388 return message === null ? [] : [message];
389 }),
390 coverage,
391 budget: tokenCap,
392 });
393 return {
394 id: handoffId,
395 transferId: null,
396 threadId: input.threadId,
397 targetRunId: input.targetRunId,
398 fromProviderThreadIds: [],
399 toProviderThreadId: input.toProviderThreadId,
400 coveredRunOrdinals: { from: 1, to: 1 },
401 strategy: "manual_context",
402 status: "ready",
403 summaryMessageId: null,
404 summaryText: makeLegacyImportSummary(input.items),
405 history: {
406 messages: selected.messages,
407 coverage,
408 omittedItems: selected.omittedItems,
409 omittedItemIds: selected.omittedItemIds,
410 },
411 createdByProviderInstanceId: null,
412 createdAt: input.createdAt,
413 updatedAt: input.createdAt,
414 } satisfies OrchestrationV2ContextHandoff;
415 },
416 );
417​
418 const prepareForkDelta = Effect.fn("orchestrationV2.contextHandoff.prepareForkDelta")(
419 function* (input: {
420 readonly sourceThreadId: ThreadId;
421 readonly targetThreadId: ThreadId;
422 readonly targetRunId: RunId;
423 readonly transferId: OrchestrationV2ContextHandoff["transferId"];
424 readonly fromProviderThreadIds: ReadonlyArray<ProviderThreadId>;
425 readonly toProviderThreadId: ProviderThreadId;
426 readonly fromProviderInstanceId: ProviderInstanceId;
427 readonly toProviderInstanceId: ProviderInstanceId;
428 readonly coveredRunOrdinals: OrchestrationV2ContextHandoff["coveredRunOrdinals"];
429 readonly deltaItems: ReadonlyArray<OrchestrationV2TurnItem>;
430 readonly createdAt: DateTime.Utc;
431 }) {
432 const handoffId = yield* idAllocator.allocate
433 .contextHandoff({
434 threadId: input.targetThreadId,
435 fromProviderInstanceId: input.fromProviderInstanceId,
436 toProviderInstanceId: input.toProviderInstanceId,
437 })
438 .pipe(
439 Effect.mapError(
440 (cause) =>
441 new ContextHandoffPrepareError({
442 threadId: input.targetThreadId,
443 targetRunId: input.targetRunId,
444 fromProviderThreadIds: Array.from(input.fromProviderThreadIds),
445 toProviderThreadId: input.toProviderThreadId,
446 cause,
447 }),
448 ),
449 );
450 const coverage = handoffCoverage({
451 threadId: input.sourceThreadId,
452 coveredRunOrdinals: input.coveredRunOrdinals,
453 items: input.deltaItems,
454 });
455 const selected = selectHistory({
456 messages: input.deltaItems.flatMap((item) => {
457 const message = historicalMessage(item);
458 return message === null ? [] : [message];
459 }),
460 coverage,
461 budget: tokenCap,
462 });
463 return {
464 id: handoffId,
465 transferId: input.transferId,
466 threadId: input.targetThreadId,
467 targetRunId: input.targetRunId,
468 fromProviderThreadIds: Array.from(input.fromProviderThreadIds),
469 toProviderThreadId: input.toProviderThreadId,
470 coveredRunOrdinals: input.coveredRunOrdinals,
471 strategy: "fork_delta_summary",
472 status: "ready",
473 summaryMessageId: null,
474 summaryText: makeForkDeltaSummary(input),
475 history: {
476 messages: selected.messages,
477 coverage,
478 omittedItems: selected.omittedItems,
479 omittedItemIds: selected.omittedItemIds,
480 },
481 createdByProviderInstanceId: null,
482 createdAt: input.createdAt,
483 updatedAt: input.createdAt,
484 } satisfies OrchestrationV2ContextHandoff;
485 },
486 );
487​
488 const prepareProviderHandoff = Effect.fn(
489 "orchestrationV2.contextHandoff.prepareProviderHandoff",
490 )(function* (input: {
491 readonly threadId: ThreadId;
492 readonly sourceThreadId?: ThreadId;
493 readonly targetRunId: RunId;
494 readonly transferId: OrchestrationV2ContextHandoff["transferId"];
495 readonly fromProviderThreadIds: ReadonlyArray<ProviderThreadId>;
496 readonly toProviderThreadId: ProviderThreadId;
497 readonly fromProviderInstanceId: ProviderInstanceId;
498 readonly toProviderInstanceId: ProviderInstanceId;
499 readonly coveredRunOrdinals: OrchestrationV2ContextHandoff["coveredRunOrdinals"];
500 readonly runs?: ReadonlyArray<OrchestrationV2Run>;
501 readonly strategy: Extract<
502 OrchestrationV2ContextHandoff["strategy"],
503 "delta_since_target_last_seen" | "full_thread_summary"
504 >;
505 readonly items: ReadonlyArray<OrchestrationV2TurnItem>;
506 readonly createdAt: DateTime.Utc;
507 }) {
508 const handoffId = yield* idAllocator.allocate
509 .contextHandoff({
510 threadId: input.threadId,
511 fromProviderInstanceId: input.fromProviderInstanceId,
512 toProviderInstanceId: input.toProviderInstanceId,
513 })
514 .pipe(
515 Effect.mapError(
516 (cause) =>
517 new ContextHandoffPrepareError({
518 threadId: input.threadId,
519 targetRunId: input.targetRunId,
520 fromProviderThreadIds: Array.from(input.fromProviderThreadIds),
521 toProviderThreadId: input.toProviderThreadId,
522 cause,
523 }),
524 ),
525 );
526 const runStatuses = new Map(input.runs?.map((run) => [run.id, run.status]));
527 const coverage = handoffCoverage({
528 ...input,
529 threadId: input.sourceThreadId ?? input.threadId,
530 });
531 const selected = selectHistory({
532 messages: input.items.flatMap((item) => {
533 const message = historicalMessage(item);
534 return message === null
535 ? []
536 : [
537 {
538 ...message,
539 ...(item.runId === null ? {} : { runStatus: runStatuses.get(item.runId) }),
540 },
541 ];
542 }),
543 coverage,
544 budget: tokenCap,
545 });
546 return {
547 id: handoffId,
548 transferId: input.transferId,
549 threadId: input.threadId,
550 targetRunId: input.targetRunId,
551 fromProviderThreadIds: Array.from(input.fromProviderThreadIds),
552 toProviderThreadId: input.toProviderThreadId,
553 coveredRunOrdinals: input.coveredRunOrdinals,
554 strategy: input.strategy,
555 status: "ready",
556 summaryMessageId: null,
557 summaryText: renderHistory(selected.messages, selected.context),
558 history: {
559 messages: selected.messages,
560 coverage,
561 omittedItems: selected.omittedItems,
562 omittedItemIds: selected.omittedItemIds,
563 },
564 createdByProviderInstanceId: null,
565 createdAt: input.createdAt,
566 updatedAt: input.createdAt,
567 } satisfies OrchestrationV2ContextHandoff;
568 });
569​
570 const prepareSideChatContext = Effect.fn(
571 "orchestrationV2.contextHandoff.prepareSideChatContext",
572 )(function* (input: Parameters<ContextHandoffServiceV2Shape["prepareSideChatContext"]>[0]) {
573 const handoffId = yield* idAllocator.allocate
574 .contextHandoff({
575 threadId: input.threadId,
576 fromProviderInstanceId: input.fromProviderInstanceId,
577 toProviderInstanceId: input.toProviderInstanceId,
578 })
579 .pipe(
580 Effect.mapError(
581 (cause) =>
582 new ContextHandoffPrepareError({
583 threadId: input.threadId,
584 targetRunId: input.targetRunId,
585 fromProviderThreadIds: [],
586 toProviderThreadId: input.toProviderThreadId,
587 cause,
588 }),
589 ),
590 );
591 return {
592 id: handoffId,
593 transferId: input.transferId,
594 threadId: input.threadId,
595 targetRunId: input.targetRunId,
596 fromProviderThreadIds: [],
597 toProviderThreadId: input.toProviderThreadId,
598 coveredRunOrdinals: input.coveredRunOrdinals,
599 strategy: "manual_context",
600 status: "ready",
601 summaryMessageId: null,
602 summaryText: makeSideChatContext({
603 parentThreadId: input.parentThreadId,
604 digest: input.parentItems,
605 }),
606 createdByProviderInstanceId: null,
607 createdAt: input.createdAt,
608 updatedAt: input.createdAt,
609 } satisfies OrchestrationV2ContextHandoff;
610 });
611​
612 return ContextHandoffServiceV2.of({
613 prepareLegacyImport,
614 prepareForkDelta,
615 prepareProviderHandoff,
616 prepareSideChatContext,
617 });
618 },
619);
620​
621export const layer: Layer.Layer<ContextHandoffServiceV2, never, IdAllocator.IdAllocatorV2> =
622 Layer.effect(ContextHandoffServiceV2, makeContextHandoffService());