History

fix: reconcile main's round-18 features after the rebase

Adopts the round's main features into the v2 architecture: the #9023 media rework (streamed videos, media-file assets, protocol-relative links), #9098 shared live-activity row folded into the v2 working and thinking rows, the #9084/#9078 Claude model catalog for v2 consumers, a native #9005 OpenCode child-session abort in the v2 adapter, #9013's landed LegendList patch, and per-environment sidebar provider entries. For #8600 the server-side pieces land, but auto-settle evaluation stays client-side (reading the new server-owned settings) until the v2 orchestrator grows its own settlement reactor; main's v1-only reactor and coalescer additions are dropped with the rest of the v1 path.

Julius Marminge committed 2 months ago

Commit 7b13bca1 parent 786a25847 files · +1855 −2616Browse files

Showing the changes to packages/client-runtime/src (5 of 47 files). Show all

Unified · Split

5 files
  1. packages/client-runtime/src/state/threadSettled.test.ts +587 −0
  2. packages/client-runtime/src/state/threadSettled.ts +0 −1
  3. packages/client-runtime/src/state/threads-sync.test.ts +6 −3
  4. packages/client-runtime/src/work-log/presentation.test.ts +1 −1
  5. packages/client-runtime/src/work-log/presentation.ts +2 −2
packages/client-runtime/src/state/threadSettled.test.ts+587−0
@@ -0,0 +1,587 @@
1+import {
2+ ProjectId,
3+ ProviderInstanceId,
4+ ThreadId,
5+ TurnId,
6+ type OrchestrationThreadShell,
7+} from "@t3tools/contracts";
8+import { describe, expect, it } from "vite-plus/test";
9+
10+import {
11+ canSettle,
12+ changeRequestAutoSettles,
13+ effectiveSettled,
14+ hasQueuedTurnStart,
15+ threadLastActivityAt,
16+ type ChangeRequestStateLike,
17+} from "./threadSettled.ts";
18+
19+const NOW = "2026-04-10T00:00:00.000Z";
20+const FRESH = "2026-04-09T00:00:00.000Z";
21+const STALE = "2026-04-06T23:59:59.999Z";
22+
23+describe("changeRequestAutoSettles", () => {
24+ it.each([
25+ ["open", true, false],
26+ ["merged", true, true],
27+ ["merged", false, false],
28+ ["closed", false, true],
29+ [null, false, false],
30+ ] as const)("state=%s autoSettleOnMerge=%s returns %s", (state, autoSettleOnMerge, expected) => {
31+ expect(changeRequestAutoSettles(state === null ? null : { state }, { autoSettleOnMerge })).toBe(
32+ expected,
33+ );
34+ });
35+
36+ const THREAD_CREATED_AT = "2026-04-01T00:00:00.000Z";
37+ const idleThread = {
38+ createdAt: THREAD_CREATED_AT,
39+ latestUserMessageAt: null,
40+ latestTurn: null,
41+ };
42+
43+ it("ignores a terminal change request last touched before the thread existed", () => {
44+ for (const state of ["merged", "closed"] as const) {
45+ expect(
46+ changeRequestAutoSettles(
47+ { state, updatedAt: "2026-03-31T23:59:59.999Z" },
48+ { thread: idleThread },
49+ ),
50+ ).toBe(false);
51+ }
52+ });
53+
54+ it("settles on a terminal change request touched at or after the thread's latest event", () => {
55+ for (const updatedAt of [THREAD_CREATED_AT, "2026-04-02T00:00:00.000Z"]) {
56+ expect(changeRequestAutoSettles({ state: "merged", updatedAt }, { thread: idleThread })).toBe(
57+ true,
58+ );
59+ }
60+ });
61+
62+ it("never re-settles a thread revived after the merge", () => {
63+ // Settling on a merge happens once: a user message newer than the PR's
64+ // last activity means the conversation outlived the PR.
65+ const revived = {
66+ createdAt: THREAD_CREATED_AT,
67+ latestUserMessageAt: "2026-04-05T00:00:00.000Z",
68+ latestTurn: null,
69+ };
70+ expect(
71+ changeRequestAutoSettles(
72+ { state: "merged", updatedAt: "2026-04-03T00:00:00.000Z" },
73+ { thread: revived },
74+ ),
75+ ).toBe(false);
76+ // A merge landing after the revival still settles.
77+ expect(
78+ changeRequestAutoSettles(
79+ { state: "merged", updatedAt: "2026-04-06T00:00:00.000Z" },
80+ { thread: revived },
81+ ),
82+ ).toBe(true);
83+ });
84+
85+ it("still settles when the merge lands during an in-flight turn", () => {
86+ // Anchor is user-initiated activity only: the agent finishing a turn
87+ // after the merge must not block the settle the merge earned.
88+ const midTurnMerge = {
89+ createdAt: THREAD_CREATED_AT,
90+ latestUserMessageAt: "2026-04-02T00:00:00.000Z",
91+ latestTurn: {
92+ turnId: TurnId.make("turn-mid"),
93+ state: "completed" as const,
94+ requestedAt: "2026-04-02T00:00:00.000Z",
95+ startedAt: "2026-04-02T00:00:05.000Z",
96+ completedAt: "2026-04-02T00:20:00.000Z",
97+ assistantMessageId: null,
98+ },
99+ };
100+ expect(
101+ changeRequestAutoSettles(
102+ { state: "merged", updatedAt: "2026-04-02T00:10:00.000Z" },
103+ { thread: midTurnMerge },
104+ ),
105+ ).toBe(true);
106+ });
107+
108+ it("falls back to settling when either timestamp is missing or malformed", () => {
109+ expect(changeRequestAutoSettles({ state: "merged" }, { thread: idleThread })).toBe(true);
110+ expect(
111+ changeRequestAutoSettles({ state: "merged", updatedAt: null }, { thread: idleThread }),
112+ ).toBe(true);
113+ expect(
114+ changeRequestAutoSettles({ state: "merged", updatedAt: "2026-03-01T00:00:00.000Z" }, {}),
115+ ).toBe(true);
116+ expect(
117+ changeRequestAutoSettles(
118+ { state: "merged", updatedAt: "not-a-date" },
119+ { thread: idleThread },
120+ ),
121+ ).toBe(true);
122+ });
123+});
124+
125+function makeShell(input: {
126+ readonly settledOverride?: "settled" | "active" | null;
127+ readonly activityAt: string | null;
128+ readonly sessionStatus?: "starting" | "running";
129+ readonly pending?: "approval" | "user-input";
130+}): OrchestrationThreadShell {
131+ const threadId = ThreadId.make("thread-1");
132+ return {
133+ id: threadId,
134+ projectId: ProjectId.make("project-1"),
135+ title: "Thread",
136+ modelSelection: { instanceId: ProviderInstanceId.make("codex"), model: "gpt-5.4" },
137+ runtimeMode: "full-access",
138+ interactionMode: "default",
139+ branch: null,
140+ worktreePath: null,
141+ latestTurn:
142+ input.activityAt === null
143+ ? null
144+ : {
145+ turnId: TurnId.make("turn-1"),
146+ state: "completed",
147+ requestedAt: input.activityAt,
148+ startedAt: null,
149+ completedAt: null,
150+ assistantMessageId: null,
151+ },
152+ createdAt: "2026-04-01T00:00:00.000Z",
153+ updatedAt: NOW,
154+ archivedAt: null,
155+ settledOverride: input.settledOverride ?? null,
156+ settledAt: input.settledOverride === "settled" ? NOW : null,
157+ session:
158+ input.sessionStatus === undefined
159+ ? null
160+ : {
161+ threadId,
162+ status: input.sessionStatus,
163+ providerName: "Codex",
164+ runtimeMode: "full-access",
165+ activeTurnId: null,
166+ lastError: null,
167+ updatedAt: NOW,
168+ },
169+ latestUserMessageAt: null,
170+ hasPendingApprovals: input.pending === "approval",
171+ hasPendingUserInput: input.pending === "user-input",
172+ hasActionableProposedPlan: false,
173+ };
174+}
175+
176+describe("threadLastActivityAt", () => {
177+ it("returns the latest real user or turn activity and ignores thread/session updates", () => {
178+ const shell = makeShell({ activityAt: null, sessionStatus: "running" });
179+ const withActivity: OrchestrationThreadShell = {
180+ ...shell,
181+ latestUserMessageAt: "2026-04-04T00:00:00.000Z",
182+ latestTurn: {
183+ turnId: TurnId.make("turn-1"),
184+ state: "completed",
185+ requestedAt: "2026-04-03T00:00:00.000Z",
186+ startedAt: "2026-04-05T00:00:00.000Z",
187+ completedAt: "2026-04-06T00:00:00.000Z",
188+ assistantMessageId: null,
189+ },
190+ };
191+
192+ expect(threadLastActivityAt(withActivity)).toBe("2026-04-06T00:00:00.000Z");
193+ expect(threadLastActivityAt(shell)).toBeNull();
194+ });
195+});
196+
197+describe("effectiveSettled", () => {
198+ const overrideCases = [null, "settled", "active"] as const;
199+ const changeRequestStates = [undefined, "open", "merged"] as const;
200+ const inactivityCases = [
201+ ["fresh", FRESH],
202+ ["stale", STALE],
203+ ["no-activity", null],
204+ ] as const;
205+ const runningCases = [false, true] as const;
206+ const pendingCases = [undefined, "approval", "user-input"] as const;
207+ const truthTable = overrideCases.flatMap((settledOverride) =>
208+ changeRequestStates.flatMap((changeRequestState) =>
209+ inactivityCases.flatMap(([inactivity, activityAt]) =>
210+ runningCases.flatMap((running) =>
211+ pendingCases.map((pending) => ({
212+ settledOverride,
213+ changeRequestState,
214+ inactivity,
215+ activityAt,
216+ running,
217+ pending,
218+ // Settled iff nothing blocks (pending work / live session) AND
219+ // the override says settled, or (with no override) a merged PR
220+ // or staleness auto-settles. The "active" pin suppresses both
221+ // auto signals, and an open PR suppresses the inactivity path:
222+ // a thread with a PR out for review is never done, however quiet.
223+ expected:
224+ pending === undefined &&
225+ !running &&
226+ (settledOverride === "settled" ||
227+ (settledOverride === null &&
228+ (changeRequestState === "merged" ||
229+ (changeRequestState !== "open" && inactivity === "stale")))),
230+ })),
231+ ),
232+ ),
233+ ),
234+ );
235+
236+ it.each(truthTable)(
237+ "override=$settledOverride pr=$changeRequestState inactivity=$inactivity running=$running pending=$pending",
238+ ({ settledOverride, changeRequestState, activityAt, running, pending, expected }) => {
239+ const shell = makeShell({
240+ settledOverride,
241+ activityAt,
242+ ...(running ? { sessionStatus: "running" as const } : {}),
243+ ...(pending === undefined ? {} : { pending }),
244+ });
245+ const changeRequestOptions =
246+ changeRequestState === undefined
247+ ? {}
248+ : { changeRequest: { state: changeRequestState as ChangeRequestStateLike } };
249+
250+ expect(
251+ effectiveSettled(shell, {
252+ now: NOW,
253+ autoSettleAfterDays: 3,
254+ ...changeRequestOptions,
255+ }),
256+ ).toBe(expected);
257+ },
258+ );
259+
260+ it("treats closed change requests like merged ones", () => {
261+ const shell = makeShell({ activityAt: null });
262+ expect(
263+ effectiveSettled(shell, {
264+ now: NOW,
265+ autoSettleAfterDays: null,
266+ changeRequest: { state: "closed" },
267+ }),
268+ ).toBe(true);
269+ });
270+
271+ it("settles immediately when a change request merges or closes", () => {
272+ const recentlyActive = makeShell({ activityAt: "2026-04-09T23:59:59.999Z" });
273+ for (const changeRequestState of ["merged", "closed"] as const) {
274+ expect(
275+ effectiveSettled(recentlyActive, {
276+ now: NOW,
277+ autoSettleAfterDays: null,
278+ changeRequest: { state: changeRequestState },
279+ }),
280+ ).toBe(true);
281+ }
282+ });
283+
284+ it("ignores a change request that merged before the thread's latest event", () => {
285+ // A new thread started at a worktree root inherits the branch's old
286+ // merged PR, and a revived thread outlives its merge; neither settles
287+ // the live conversation.
288+ const fresh = makeShell({ activityAt: FRESH });
289+ for (const state of ["merged", "closed"] as const) {
290+ expect(
291+ effectiveSettled(fresh, {
292+ now: NOW,
293+ autoSettleAfterDays: null,
294+ changeRequest: { state, updatedAt: "2026-03-20T00:00:00.000Z" },
295+ }),
296+ ).toBe(false);
297+ }
298+ // A merge during the thread's life still settles it.
299+ expect(
300+ effectiveSettled(fresh, {
301+ now: NOW,
302+ autoSettleAfterDays: null,
303+ changeRequest: { state: "merged", updatedAt: "2026-04-09T00:00:00.000Z" },
304+ }),
305+ ).toBe(true);
306+ });
307+
308+ it("can keep a merged change request active", () => {
309+ const recentlyActive = makeShell({ activityAt: "2026-04-09T23:59:59.999Z" });
310+ expect(
311+ effectiveSettled(recentlyActive, {
312+ now: NOW,
313+ autoSettleAfterDays: null,
314+ autoSettleOnMerge: false,
315+ changeRequest: { state: "merged" },
316+ }),
317+ ).toBe(false);
318+
319+ expect(
320+ effectiveSettled(recentlyActive, {
321+ now: NOW,
322+ autoSettleAfterDays: null,
323+ autoSettleOnMerge: false,
324+ changeRequest: { state: "closed" },
325+ }),
326+ ).toBe(true);
327+ });
328+
329+ it("never auto-settles a stale thread with an open change request", () => {
330+ const stale = makeShell({ activityAt: STALE });
331+ expect(
332+ effectiveSettled(stale, {
333+ now: NOW,
334+ autoSettleAfterDays: 3,
335+ changeRequest: { state: "open" },
336+ }),
337+ ).toBe(false);
338+ // An explicit user settle still wins: open PR only blocks the auto path.
339+ const settled = makeShell({ settledOverride: "settled", activityAt: STALE });
340+ expect(
341+ effectiveSettled(settled, {
342+ now: NOW,
343+ autoSettleAfterDays: 3,
344+ changeRequest: { state: "open" },
345+ }),
346+ ).toBe(true);
347+ });
348+
349+ it("keeps an explicitly un-settled merged-PR thread active", () => {
350+ const shell = makeShell({
351+ settledOverride: "active",
352+ activityAt: "2026-04-09T23:59:59.999Z",
353+ });
354+ expect(
355+ effectiveSettled(shell, {
356+ now: NOW,
357+ autoSettleAfterDays: null,
358+ changeRequest: { state: "merged" },
359+ }),
360+ ).toBe(false);
361+ });
362+
363+ it("never settles a starting session, even with a settled override", () => {
364+ const shell = makeShell({
365+ settledOverride: "settled",
366+ activityAt: STALE,
367+ sessionStatus: "starting",
368+ });
369+ expect(
370+ effectiveSettled(shell, {
371+ now: NOW,
372+ autoSettleAfterDays: 3,
373+ changeRequest: { state: "merged" },
374+ }),
375+ ).toBe(false);
376+ });
377+
378+ it("keeps a new turn active from queued through starting and running", () => {
379+ const requestedAt = "2026-04-09T12:00:00.000Z";
380+ const transitionNow = "2026-04-09T12:00:30.000Z";
381+ const base = makeShell({
382+ settledOverride: null,
383+ activityAt: STALE,
384+ });
385+ const queued: OrchestrationThreadShell = {
386+ ...base,
387+ latestUserMessageAt: requestedAt,
388+ latestTurn: null,
389+ session: null,
390+ };
391+ const starting: OrchestrationThreadShell = {
392+ ...queued,
393+ session: {
394+ threadId: queued.id,
395+ status: "starting",
396+ providerName: "Codex",
397+ runtimeMode: "full-access",
398+ activeTurnId: null,
399+ lastError: null,
400+ updatedAt: requestedAt,
401+ },
402+ };
403+ const running: OrchestrationThreadShell = {
404+ ...starting,
405+ session: {
406+ ...starting.session!,
407+ status: "running",
408+ activeTurnId: TurnId.make("turn-new"),
409+ },
410+ };
411+
412+ for (const shell of [queued, starting, running]) {
413+ expect(
414+ effectiveSettled(shell, {
415+ now: transitionNow,
416+ autoSettleAfterDays: 3,
417+ changeRequest: { state: "merged" },
418+ }),
419+ ).toBe(false);
420+ }
421+ });
422+
423+ it("uses a strict inactivity boundary and honors a null threshold", () => {
424+ const boundary = makeShell({
425+ activityAt: "2026-04-07T00:00:00.000Z",
426+ });
427+ const stale = makeShell({ activityAt: STALE });
428+
429+ expect(effectiveSettled(boundary, { now: NOW, autoSettleAfterDays: 3 })).toBe(false);
430+ expect(effectiveSettled(stale, { now: NOW, autoSettleAfterDays: null })).toBe(false);
431+ });
432+});
433+
434+describe("hasQueuedTurnStart", () => {
435+ const QUEUED_AT = "2026-04-09T12:00:00.000Z";
436+ // Within the adoption grace window of the queued message.
437+ const JUST_AFTER = { now: "2026-04-09T12:00:30.000Z" };
438+
439+ it("flags a user message no turn has picked up, within the grace window", () => {
440+ const noTurn = { latestUserMessageAt: QUEUED_AT, latestTurn: null, session: null };
441+ expect(hasQueuedTurnStart(noTurn, JUST_AFTER)).toBe(true);
442+
443+ const staleTurn = {
444+ ...makeShell({ activityAt: FRESH }),
445+ latestUserMessageAt: QUEUED_AT,
446+ };
447+ expect(hasQueuedTurnStart(staleTurn, JUST_AFTER)).toBe(true);
448+ });
449+
450+ it("expires after the grace window: an unadopted message is a failed start, not queued work", () => {
451+ const noTurn = { latestUserMessageAt: QUEUED_AT, latestTurn: null, session: null };
452+ expect(hasQueuedTurnStart(noTurn, { now: "2026-04-09T12:03:00.000Z" })).toBe(false);
453+ // Historical shells (e.g. from servers that never carried latestTurn)
454+ // must never read as queued.
455+ expect(hasQueuedTurnStart(noTurn, { now: NOW })).toBe(false);
456+ });
457+
458+ it("clears once a turn adopts the message or the start fails", () => {
459+ const adopted = {
460+ ...makeShell({ activityAt: QUEUED_AT }),
461+ latestUserMessageAt: QUEUED_AT,
462+ };
463+ expect(hasQueuedTurnStart(adopted, JUST_AFTER)).toBe(false);
464+
465+ const failed = makeShell({ activityAt: FRESH });
466+ const failedShell = {
467+ ...failed,
468+ latestUserMessageAt: QUEUED_AT,
469+ session: {
470+ threadId: failed.id,
471+ status: "error" as const,
472+ providerName: "Codex",
473+ runtimeMode: "full-access" as const,
474+ activeTurnId: null,
475+ lastError: "boom",
476+ updatedAt: NOW,
477+ },
478+ };
479+ expect(hasQueuedTurnStart(failedShell, JUST_AFTER)).toBe(false);
480+ });
481+
482+ it("is quiet without user messages", () => {
483+ expect(hasQueuedTurnStart(makeShell({ activityAt: FRESH }), JUST_AFTER)).toBe(false);
484+ });
485+
486+ it("bounds the grace window in both directions: a future-stamped message is skew, not queued work", () => {
487+ // Message timestamps originate on other devices; a clock an hour ahead
488+ // must not hold the queued state for the whole skew.
489+ const skewed = {
490+ latestUserMessageAt: "2026-04-09T13:00:00.000Z",
491+ latestTurn: null,
492+ session: null,
493+ };
494+ expect(hasQueuedTurnStart(skewed, { now: "2026-04-09T12:00:00.000Z" })).toBe(false);
495+ // A small negative age (within the grace window) still reads as queued.
496+ const slightlyAhead = {
497+ latestUserMessageAt: "2026-04-09T12:00:30.000Z",
498+ latestTurn: null,
499+ session: null,
500+ };
501+ expect(hasQueuedTurnStart(slightlyAhead, { now: "2026-04-09T12:00:00.000Z" })).toBe(true);
502+ });
503+});
504+
505+describe("canSettle", () => {
506+ it("blocks every state effectiveSettled refuses to classify as settled", () => {
507+ expect(canSettle(makeShell({ activityAt: FRESH }), { now: NOW })).toBe(true);
508+ expect(
509+ canSettle(makeShell({ activityAt: FRESH, sessionStatus: "starting" }), { now: NOW }),
510+ ).toBe(false);
511+ expect(
512+ canSettle(makeShell({ activityAt: FRESH, sessionStatus: "running" }), { now: NOW }),
513+ ).toBe(false);
514+ expect(canSettle(makeShell({ activityAt: FRESH, pending: "approval" }), { now: NOW })).toBe(
515+ false,
516+ );
517+ expect(canSettle(makeShell({ activityAt: FRESH, pending: "user-input" }), { now: NOW })).toBe(
518+ false,
519+ );
520+ });
521+
522+ it("blocks settling a queued turn start, only within the grace window", () => {
523+ const queued = {
524+ ...makeShell({ activityAt: FRESH }),
525+ latestUserMessageAt: "2026-04-09T12:00:00.000Z",
526+ };
527+ const justAfter = "2026-04-09T12:00:30.000Z";
528+ expect(canSettle(queued, { now: justAfter })).toBe(false);
529+ // effectiveSettled must agree: queued work never auto-settles either,
530+ // even with a merged PR.
531+ expect(
532+ effectiveSettled(queued, {
533+ now: justAfter,
534+ autoSettleAfterDays: 3,
535+ changeRequest: { state: "merged" },
536+ }),
537+ ).toBe(false);
538+ // Past the window the message is a failed/stale start: settleable again.
539+ expect(canSettle(queued, { now: NOW })).toBe(true);
540+ });
541+
542+ it("lets a server-accepted settle overrule the clock-derived queued blocker", () => {
543+ // The settle action ran with wall-clock `now` (past the grace window);
544+ // the list partition re-evaluates with a minute-floored `now` that is
545+ // still INSIDE the window. settledAt >= message time proves the server
546+ // already adjudicated this exact message, so the row must not snap back
547+ // to active until the coarser clock catches up.
548+ const messageAt = "2026-04-09T12:00:00.000Z";
549+ const flooredNow = "2026-04-09T12:01:00.000Z";
550+ const base = makeShell({ settledOverride: "settled", activityAt: null });
551+ const settledAfterMessage = {
552+ ...base,
553+ latestUserMessageAt: messageAt,
554+ settledAt: "2026-04-09T12:02:10.000Z",
555+ };
556+ expect(hasQueuedTurnStart(settledAfterMessage, { now: flooredNow })).toBe(true);
557+ expect(effectiveSettled(settledAfterMessage, { now: flooredNow, autoSettleAfterDays: 3 })).toBe(
558+ true,
559+ );
560+
561+ // A message NEWER than settledAt is genuinely new work: still blocked
562+ // until the server's auto-unsettle lands.
563+ const messageAfterSettle = {
564+ ...base,
565+ latestUserMessageAt: "2026-04-09T12:03:00.000Z",
566+ settledAt: "2026-04-09T12:02:10.000Z",
567+ };
568+ expect(
569+ effectiveSettled(messageAfterSettle, {
570+ now: "2026-04-09T12:03:30.000Z",
571+ autoSettleAfterDays: 3,
572+ }),
573+ ).toBe(false);
574+ });
575+
576+ it("agrees with effectiveSettled's blockers for explicitly settled shells", () => {
577+ // Anything canSettle rejects must render as active even when the user
578+ // settled it earlier.
579+ const blocked = makeShell({
580+ settledOverride: "settled",
581+ activityAt: FRESH,
582+ pending: "user-input",
583+ });
584+ expect(canSettle(blocked, { now: NOW })).toBe(false);
585+ expect(effectiveSettled(blocked, { now: NOW, autoSettleAfterDays: 3 })).toBe(false);
586+ });
587+});
packages/client-runtime/src/state/threadSettled.ts+0−1
@@ -138,7 +138,6 @@ export function threadLastActivityAt(shell: SettlementThreadShell): string | nul · 137 unmodified lines
138138 * such threads would be permanently unsettleable.
139139 */
140140 export const QUEUED_TURN_START_GRACE_MS = 2 * 60 * 1_000;
141−const DAY_MS = 24 * 60 * 60 * 1_000;
142141
143142 /**
144143 * A user message no turn has picked up yet: the turn.start command was
packages/client-runtime/src/state/threads-sync.test.ts+6−3
@@ -70,9 +70,12 @@ function testSession( · 69 unmodified lines
7070 ): RpcSession.RpcSession {
7171 return {
7272 client,
73− initialConfig: Effect.succeed({
74− threadResumeCompletionMarker: config?.completionMarker === true,
75− } as never),
73+ initialConfig: Effect.succeed(
74+ config?.completionMarker === true
75+ ? ({ threadResumeCompletionMarker: true } as never)
76+ : ({} as never),
77+ ),
78+ subscribeServerConfig: (input) => client.subscribeServerConfig(input),
7679 ready: Effect.void,
7780 probe: Effect.void,
7881 closed: Effect.never,
packages/client-runtime/src/work-log/presentation.test.ts+1−1
@@ -444,7 +444,7 @@ describe("resolveViewedImageAsset", () => { · 443 unmodified lines
444444 }),
445445 ).toEqual({
446446 resource: {
447− _tag: "workspace-file",
447+ _tag: "media-file",
448448 threadId,
449449 path: "/workspace/screens/logo.svg",
450450 },
packages/client-runtime/src/work-log/presentation.ts+2−2
@@ -135,7 +135,7 @@ export function workEntryViewedImagePath(entry: WorkLogPresentationEntry): strin · 134 unmodified lines
135135 }
136136
137137 export interface ViewedImageAsset {
138− readonly resource: Extract<AssetResource, { readonly _tag: "attachment" | "workspace-file" }>;
138+ readonly resource: Extract<AssetResource, { readonly _tag: "attachment" | "media-file" }>;
139139 readonly alt: string;
140140 readonly srcFragment: string;
141141 }
@@ -165,7 +165,7 @@ export function resolveViewedImageAsset( · 23 unmodified lines
165165 return {
166166 resource: attachmentId
167167 ? { _tag: "attachment", attachmentId }
168− : { _tag: "workspace-file", threadId: input.threadId, path },
168+ : { _tag: "media-file", threadId: input.threadId, path },
169169 alt: path.split(/[\\/]/).at(-1) ?? "image",
170170 srcFragment: markdownImageSourceFragment(source),
171171 };