authorgravatar for maria@kuuro.netmaria <maria@kuuro.net> 2026-09-02 18:58:07-04:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-02 18:58:07-04:00
log63f334baf3432c4404ddfc9e71d33bf16575f0ed
treea868ddf8cc912c2ae8921964e29f219103884f95
parent064392ffc7fc58fcc3018fd2ba2df33fbb21f1de
signature Signed by PGP key B5690EEEBB952194

Revert "feat(providers): add context compaction across harnesses" (#9284)


35 files changed, 114 insertions(+), 646 deletions(-)

apps/mobile/src/features/threads/use-composer-command-menu.ts+2-2
...@@ -127,8 +127,8 @@ export function useComposerCommandMenu({...@@ -127,8 +127,8 @@ export function useComposerCommandMenu({
127 // Codex feedback uploads an existing thread's session and logs.127 // Codex feedback uploads an existing thread's session and logs.
128 if (128 if (
129 !hasThread &&129 !hasThread &&
130 (command.name === "compact" ||130 selectedProviderStatus?.driver === "codex" &&
131 (selectedProviderStatus?.driver === "codex" && command.name === "feedback"))131 command.name === "feedback"
132 ) {132 ) {
133 continue;133 continue;
134 }134 }
apps/server/integration/orphanedProviderSessionStartup.integration.test.ts-1
...@@ -110,7 +110,6 @@ const startupDependencies = Layer.mergeAll(...@@ -110,7 +110,6 @@ const startupDependencies = Layer.mergeAll(
110 Layer.succeed(ProviderService.ProviderService, {110 Layer.succeed(ProviderService.ProviderService, {
111 startSession: () => Effect.die("unused"),111 startSession: () => Effect.die("unused"),
112 sendTurn: () => Effect.die("unused"),112 sendTurn: () => Effect.die("unused"),
113 compactThread: () => Effect.die("unused"),
114 interruptTurn: () => Effect.die("unused"),113 interruptTurn: () => Effect.die("unused"),
115 respondToRequest: () => Effect.die("unused"),114 respondToRequest: () => Effect.die("unused"),
116 respondToUserInput: () => Effect.die("unused"),115 respondToUserInput: () => Effect.die("unused"),
apps/server/src/orchestration/Layers/CheckpointReactor.test.ts-1
...@@ -107,7 +107,6 @@ function createProviderServiceHarness(...@@ -107,7 +107,6 @@ function createProviderServiceHarness(
107 const service: ProviderServiceShape = {107 const service: ProviderServiceShape = {
108 startSession: () => unsupported(),108 startSession: () => unsupported(),
109 sendTurn: () => unsupported(),109 sendTurn: () => unsupported(),
110 compactThread: () => unsupported(),
111 interruptTurn: () => unsupported(),110 interruptTurn: () => unsupported(),
112 respondToRequest: () => unsupported(),111 respondToRequest: () => unsupported(),
113 respondToUserInput: () => unsupported(),112 respondToUserInput: () => unsupported(),
apps/server/src/orchestration/Layers/ProjectionPipeline.ts-15
...@@ -1275,21 +1275,6 @@ const makeOrchestrationProjectionPipeline = Effect.fn("makeOrchestrationProjecti...@@ -1275,21 +1275,6 @@ const makeOrchestrationProjectionPipeline = Effect.fn("makeOrchestrationProjecti
1275 return;1275 return;
1276 }1276 }
12771277
1278 case "thread.activity-appended":
1279 if (event.payload.activity.kind !== "provider.turn.start.failed") return;
1280 const pendingTurnStart = yield* projectionTurnRepository.getPendingTurnStartByThreadId(
1281 event.payload,
1282 );
1283 if (
1284 Option.isNone(pendingTurnStart) ||
1285 String(pendingTurnStart.value.messageId) !==
1286 extractActivityRequestId(event.payload.activity.payload)
1287 ) {
1288 return;
1289 }
1290 yield* projectionTurnRepository.deletePendingTurnStartByThreadId(event.payload);
1291 return;
1292
1293 case "thread.session-set": {1278 case "thread.session-set": {
1294 const turnId = event.payload.session.activeTurnId;1279 const turnId = event.payload.session.activeTurnId;
1295 if (turnId === null || event.payload.session.status !== "running") {1280 if (turnId === null || event.payload.session.status !== "running") {
apps/server/src/orchestration/Layers/ProviderCommandReactor.test.ts-24
...@@ -254,7 +254,6 @@ describe("ProviderCommandReactor", () => {...@@ -254,7 +254,6 @@ describe("ProviderCommandReactor", () => {
254 turnId: asTurnId("turn-1"),254 turnId: asTurnId("turn-1"),
255 }),255 }),
256 );256 );
257 const compactThread = vi.fn((_: ThreadId) => Effect.void);
258 const interruptTurn = vi.fn((_: unknown) => input?.interruptTurnEffect?.() ?? Effect.void);257 const interruptTurn = vi.fn((_: unknown) => input?.interruptTurnEffect?.() ?? Effect.void);
259 const respondToRequest = vi.fn<ProviderServiceShape["respondToRequest"]>(() => Effect.void);258 const respondToRequest = vi.fn<ProviderServiceShape["respondToRequest"]>(() => Effect.void);
260 const respondToUserInput = vi.fn<ProviderServiceShape["respondToUserInput"]>(() => Effect.void);259 const respondToUserInput = vi.fn<ProviderServiceShape["respondToUserInput"]>(() => Effect.void);
...@@ -340,7 +339,6 @@ describe("ProviderCommandReactor", () => {...@@ -340,7 +339,6 @@ describe("ProviderCommandReactor", () => {
340 const service: ProviderServiceShape = {339 const service: ProviderServiceShape = {
341 startSession: startSession as ProviderServiceShape["startSession"],340 startSession: startSession as ProviderServiceShape["startSession"],
342 sendTurn: sendTurn as ProviderServiceShape["sendTurn"],341 sendTurn: sendTurn as ProviderServiceShape["sendTurn"],
343 compactThread,
344 interruptTurn: interruptTurn as ProviderServiceShape["interruptTurn"],342 interruptTurn: interruptTurn as ProviderServiceShape["interruptTurn"],
345 respondToRequest: respondToRequest as ProviderServiceShape["respondToRequest"],343 respondToRequest: respondToRequest as ProviderServiceShape["respondToRequest"],
346 respondToUserInput: respondToUserInput as ProviderServiceShape["respondToUserInput"],344 respondToUserInput: respondToUserInput as ProviderServiceShape["respondToUserInput"],
...@@ -535,7 +533,6 @@ describe("ProviderCommandReactor", () => {...@@ -535,7 +533,6 @@ describe("ProviderCommandReactor", () => {
535 readModel: () => Effect.runPromise(snapshotQuery.getSnapshot()),533 readModel: () => Effect.runPromise(snapshotQuery.getSnapshot()),
536 startSession,534 startSession,
537 sendTurn,535 sendTurn,
538 compactThread,
539 interruptTurn,536 interruptTurn,
540 respondToRequest,537 respondToRequest,
541 respondToUserInput,538 respondToUserInput,
...@@ -635,27 +632,6 @@ describe("ProviderCommandReactor", () => {...@@ -635,27 +632,6 @@ describe("ProviderCommandReactor", () => {
635 }),632 }),
636 );633 );
637634
638 effectIt.effect("rejects /compact without conversation context", () =>
639 Effect.gen(function* () {
640 const harness = yield* Effect.promise(() => createHarness());
641 yield* harness.engine.dispatch({
642 type: "thread.turn.start",
643 commandId: CommandId.make("cmd-empty-compact"),
644 threadId: ThreadId.make("thread-1"),
645 message: {
646 messageId: asMessageId("user-message-empty-compact"),
647 role: "user",
648 text: "/compact",
649 attachments: [],
650 },
651 interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE,
652 runtimeMode: "approval-required",
653 createdAt: "2026-01-01T00:00:00.000Z",
654 });
655 yield* Effect.promise(() => harness.drain());
656 expect(harness.compactThread).not.toHaveBeenCalled();
657 }),
658 );
659 effectIt.effect("projects starting before a slow provider session finishes", () =>635 effectIt.effect("projects starting before a slow provider session finishes", () =>
660 Effect.gen(function* () {636 Effect.gen(function* () {
661 const releaseStart = yield* Deferred.make<void>();637 const releaseStart = yield* Deferred.make<void>();
apps/server/src/orchestration/Layers/ProviderCommandReactor.ts+21-96
...@@ -30,10 +30,7 @@ import { makeDrainableWorker } from "@t3tools/shared/DrainableWorker";...@@ -30,10 +30,7 @@ import { makeDrainableWorker } from "@t3tools/shared/DrainableWorker";
3030
31import { resolveThreadWorkspaceCwd } from "../../checkpointing/Utils.ts";31import { resolveThreadWorkspaceCwd } from "../../checkpointing/Utils.ts";
32import { increment, orchestrationEventsProcessedTotal } from "../../observability/Metrics.ts";32import { increment, orchestrationEventsProcessedTotal } from "../../observability/Metrics.ts";
33import {33import { ProviderAdapterRequestError } from "../../provider/Errors.ts";
34 ProviderAdapterRequestError,
35 ProviderAdapterValidationError,
36} from "../../provider/Errors.ts";
37import type { ProviderServiceError } from "../../provider/Errors.ts";34import type { ProviderServiceError } from "../../provider/Errors.ts";
38import { TextGeneration } from "../../textGeneration/TextGeneration.ts";35import { TextGeneration } from "../../textGeneration/TextGeneration.ts";
39import { ProviderService } from "../../provider/Services/ProviderService.ts";36import { ProviderService } from "../../provider/Services/ProviderService.ts";
...@@ -53,7 +50,6 @@ import {...@@ -53,7 +50,6 @@ import {
53import { VcsStatusBroadcaster } from "../../vcs/VcsStatusBroadcaster.ts";50import { VcsStatusBroadcaster } from "../../vcs/VcsStatusBroadcaster.ts";
54import { GitWorkflowService } from "../../git/GitWorkflowService.ts";51import { GitWorkflowService } from "../../git/GitWorkflowService.ts";
55const isProviderAdapterRequestError = Schema.is(ProviderAdapterRequestError);52const isProviderAdapterRequestError = Schema.is(ProviderAdapterRequestError);
56const isProviderAdapterValidationError = Schema.is(ProviderAdapterValidationError);
57const isProviderDriverKind = Schema.is(ProviderDriverKind);53const isProviderDriverKind = Schema.is(ProviderDriverKind);
5854
59type ProviderIntentEvent = Extract<55type ProviderIntentEvent = Extract<
...@@ -76,10 +72,6 @@ function toNonEmptyProviderInput(value: string | undefined): string | undefined...@@ -76,10 +72,6 @@ function toNonEmptyProviderInput(value: string | undefined): string | undefined
76 return normalized && normalized.length > 0 ? normalized : undefined;72 return normalized && normalized.length > 0 ? normalized : undefined;
77}73}
7874
79const isCompactCommandMessage = (message: ThreadTitleMessage): boolean =>
80 message.role === "user" &&
81 (message.attachments?.length ?? 0) === 0 &&
82 message.text.trim().toLowerCase() === "/compact";
83function mapProviderSessionStatusToOrchestrationStatus(75function mapProviderSessionStatusToOrchestrationStatus(
84 status: "connecting" | "ready" | "running" | "error" | "closed",76 status: "connecting" | "ready" | "running" | "error" | "closed",
85): OrchestrationSession["status"] {77): OrchestrationSession["status"] {
...@@ -340,7 +332,6 @@ const make = Effect.gen(function* () {...@@ -340,7 +332,6 @@ const make = Effect.gen(function* () {
340 );332 );
341333
342 const threadModelSelections = new Map<string, ModelSelection>();334 const threadModelSelections = new Map<string, ModelSelection>();
343 const compactingThreadIds = new Set<ThreadId>();
344335
345 const appendProviderFailureActivity = (input: {336 const appendProviderFailureActivity = (input: {
346 readonly threadId: ThreadId;337 readonly threadId: ThreadId;
...@@ -384,11 +375,11 @@ const make = Effect.gen(function* () {...@@ -384,11 +375,11 @@ const make = Effect.gen(function* () {
384375
385 const formatFailureDetail = (cause: Cause.Cause<unknown>): string => {376 const formatFailureDetail = (cause: Cause.Cause<unknown>): string => {
386 const failReason = cause.reasons.find(Cause.isFailReason);377 const failReason = cause.reasons.find(Cause.isFailReason);
387 if (isProviderAdapterRequestError(failReason?.error)) {378 const providerError = isProviderAdapterRequestError(failReason?.error)
388 return failReason.error.detail;379 ? failReason.error
389 }380 : undefined;
390 if (isProviderAdapterValidationError(failReason?.error)) {381 if (providerError) {
391 return failReason.error.issue;382 return providerError.detail;
392 }383 }
393 return Cause.pretty(cause);384 return Cause.pretty(cause);
394 };385 };
...@@ -1146,6 +1137,7 @@ const make = Effect.gen(function* () {...@@ -1146,6 +1137,7 @@ const make = Effect.gen(function* () {
1146 if (!thread) {1137 if (!thread) {
1147 return;1138 return;
1148 }1139 }
1140
1149 const message = thread.messages.find((entry) => entry.id === event.payload.messageId);1141 const message = thread.messages.find((entry) => entry.id === event.payload.messageId);
1150 if (!message || message.role !== "user") {1142 if (!message || message.role !== "user") {
1151 yield* appendProviderFailureActivity({1143 yield* appendProviderFailureActivity({
...@@ -1155,28 +1147,15 @@ const make = Effect.gen(function* () {...@@ -1155,28 +1147,15 @@ const make = Effect.gen(function* () {
1155 detail: `User message '${event.payload.messageId}' was not found for turn start request.`,1147 detail: `User message '${event.payload.messageId}' was not found for turn start request.`,
1156 turnId: null,1148 turnId: null,
1157 createdAt: event.payload.createdAt,1149 createdAt: event.payload.createdAt,
1158 requestId: event.payload.messageId,
1159 });1150 });
1160 return;1151 return;
1161 }1152 }
1162 const appendTurnStartFailure = (summary: string, detail: string) =>
1163 appendProviderFailureActivity({
1164 threadId: event.payload.threadId,
1165 kind: "provider.turn.start.failed",
1166 summary,
1167 detail,
1168 turnId: null,
1169 createdAt: event.payload.createdAt,
1170 requestId: event.payload.messageId,
1171 });
11721153
1173 yield* ensureThreadWorktree(thread);1154 yield* ensureThreadWorktree(thread);
11741155
1175 const isCompactCommand = isCompactCommandMessage(message);1156 const isFirstUserMessageTurn =
1176 const nonCompactUserMessageCount = thread.messages.filter(1157 thread.messages.filter((entry) => entry.role === "user").length === 1;
1177 (entry) => entry.role === "user" && !isCompactCommandMessage(entry),1158 if (isFirstUserMessageTurn) {
1178 ).length;
1179 if (nonCompactUserMessageCount === 1 && !isCompactCommand) {
1180 const project = yield* resolveProject(thread.projectId);1159 const project = yield* resolveProject(thread.projectId);
1181 const generationCwd =1160 const generationCwd =
1182 resolveThreadWorkspaceCwd({1161 resolveThreadWorkspaceCwd({
...@@ -1215,7 +1194,16 @@ const make = Effect.gen(function* () {...@@ -1215,7 +1194,16 @@ const make = Effect.gen(function* () {
1215 detail,1194 detail,
1216 createdAt: event.payload.createdAt,1195 createdAt: event.payload.createdAt,
1217 }).pipe(1196 }).pipe(
1218 Effect.flatMap(() => appendTurnStartFailure("Provider turn start failed", detail)),1197 Effect.flatMap(() =>
1198 appendProviderFailureActivity({
1199 threadId: event.payload.threadId,
1200 kind: "provider.turn.start.failed",
1201 summary: "Provider turn start failed",
1202 detail,
1203 turnId: null,
1204 createdAt: event.payload.createdAt,
1205 }),
1206 ),
1219 Effect.asVoid,1207 Effect.asVoid,
1220 );1208 );
1221 };1209 };
...@@ -1232,69 +1220,6 @@ const make = Effect.gen(function* () {...@@ -1232,69 +1220,6 @@ const make = Effect.gen(function* () {
1232 ),1220 ),
1233 );1221 );
12341222
1235 const handleCompactionFailure = (cause: Cause.Cause<unknown>) => {
1236 if (Cause.hasInterruptsOnly(cause)) {
1237 return Effect.void;
1238 }
1239 const detail = formatFailureDetail(cause);
1240 return appendTurnStartFailure("Context compaction failed", detail).pipe(Effect.asVoid);
1241 };
1242 const recoverCompactionFailure = (cause: Cause.Cause<unknown>) =>
1243 handleCompactionFailure(cause).pipe(
1244 Effect.catchCause((recoveryCause) =>
1245 Effect.logWarning("provider command reactor failed to recover compaction failure", {
1246 eventType: event.type,
1247 threadId: event.payload.threadId,
1248 cause: Cause.pretty(recoveryCause),
1249 originalCause: Cause.pretty(cause),
1250 }),
1251 ),
1252 );
1253 if (isCompactCommand) {
1254 if (nonCompactUserMessageCount === 0) {
1255 return yield* appendTurnStartFailure(
1256 "Context compaction failed",
1257 "Context compaction requires an existing conversation.",
1258 );
1259 }
1260 const latestThread = yield* resolveThread(event.payload.threadId);
1261 if (
1262 compactingThreadIds.has(event.payload.threadId) ||
1263 latestThread?.session?.status === "starting" ||
1264 latestThread?.session?.status === "running"
1265 ) {
1266 yield* appendTurnStartFailure(
1267 "Context compaction failed",
1268 "Context compaction is unavailable while a provider turn is running.",
1269 );
1270 return;
1271 }
1272 compactingThreadIds.add(event.payload.threadId);
1273 yield* Effect.gen(function* () {
1274 yield* ensureSessionForThread(
1275 event.payload.threadId,
1276 event.payload.createdAt,
1277 event.payload.modelSelection !== undefined
1278 ? { modelSelection: event.payload.modelSelection }
1279 : undefined,
1280 );
1281 if (event.payload.modelSelection !== undefined) {
1282 threadModelSelections.set(event.payload.threadId, event.payload.modelSelection);
1283 }
1284 yield* providerService.compactThread(event.payload.threadId, event.payload.modelSelection);
1285 }).pipe(
1286 Effect.catchCause(recoverCompactionFailure),
1287 Effect.ensuring(Effect.sync(() => void compactingThreadIds.delete(event.payload.threadId))),
1288 Effect.forkScoped,
1289 );
1290 return;
1291 }
1292 if (compactingThreadIds.has(event.payload.threadId)) {
1293 return yield* appendTurnStartFailure(
1294 "Provider turn start failed",
1295 "Wait for context compaction to finish before sending another message.",
1296 );
1297 }
1298 const sendTurnRequest = yield* buildSendTurnRequestForThread({1223 const sendTurnRequest = yield* buildSendTurnRequestForThread({
1299 threadId: event.payload.threadId,1224 threadId: event.payload.threadId,
1300 messageText: message.text,1225 messageText: message.text,
...@@ -1315,7 +1240,7 @@ const make = Effect.gen(function* () {...@@ -1315,7 +1240,7 @@ const make = Effect.gen(function* () {
13151240
1316 yield* providerService1241 yield* providerService
1317 .sendTurn(sendTurnRequest.value)1242 .sendTurn(sendTurnRequest.value)
1318 .pipe(Effect.asVoid, Effect.catchCause(recoverTurnStartFailure), Effect.forkScoped);1243 .pipe(Effect.catchCause(recoverTurnStartFailure), Effect.forkScoped);
1319 });1244 });
13201245
1321 const processTurnInterruptRequested = Effect.fn("processTurnInterruptRequested")(function* (1246 const processTurnInterruptRequested = Effect.fn("processTurnInterruptRequested")(function* (
apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.test.ts+1-4
...@@ -105,7 +105,6 @@ function createProviderServiceHarness() {...@@ -105,7 +105,6 @@ function createProviderServiceHarness() {
105 const service: ProviderServiceShape = {105 const service: ProviderServiceShape = {
106 startSession: () => unsupported(),106 startSession: () => unsupported(),
107 sendTurn: () => unsupported(),107 sendTurn: () => unsupported(),
108 compactThread: () => unsupported(),
109 interruptTurn: () => unsupported(),108 interruptTurn: () => unsupported(),
110 respondToRequest: () => unsupported(),109 respondToRequest: () => unsupported(),
111 respondToUserInput: () => unsupported(),110 respondToUserInput: () => unsupported(),
...@@ -3222,8 +3221,6 @@ describe("ProviderRuntimeIngestion", () => {...@@ -3222,8 +3221,6 @@ describe("ProviderRuntimeIngestion", () => {
3222 turnId: asTurnId("turn-1"),3221 turnId: asTurnId("turn-1"),
3223 payload: {3222 payload: {
3224 state: "compacted",3223 state: "compacted",
3225 beforeTokens: 899_000,
3226 afterTokens: 19_000,
3227 detail: { source: "provider" },3224 detail: { source: "provider" },
3228 },3225 },
3229 });3226 });
...@@ -3237,7 +3234,7 @@ describe("ProviderRuntimeIngestion", () => {...@@ -3237,7 +3234,7 @@ describe("ProviderRuntimeIngestion", () => {
3237 const activity = thread.activities.find(3234 const activity = thread.activities.find(
3238 (candidate: ProviderRuntimeTestActivity) => candidate.kind === "context-compaction",3235 (candidate: ProviderRuntimeTestActivity) => candidate.kind === "context-compaction",
3239 );3236 );
3240 expect(activity?.summary).toBe("Compacted context 899K → 19K tokens");3237 expect(activity?.summary).toBe("Context compacted");
3241 expect(activity?.tone).toBe("info");3238 expect(activity?.tone).toBe("info");
3242 });3239 });
32433240
apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts+1-10
...@@ -28,7 +28,6 @@ import * as Layer from "effect/Layer";...@@ -28,7 +28,6 @@ import * as Layer from "effect/Layer";
28import * as Option from "effect/Option";28import * as Option from "effect/Option";
29import * as Stream from "effect/Stream";29import * as Stream from "effect/Stream";
30import { makeDrainableWorker } from "@t3tools/shared/DrainableWorker";30import { makeDrainableWorker } from "@t3tools/shared/DrainableWorker";
31import { formatTokens } from "@t3tools/shared/usageFormat";
3231
33import { ProviderService } from "../../provider/Services/ProviderService.ts";32import { ProviderService } from "../../provider/Services/ProviderService.ts";
34import { ProjectionTurnRepository } from "../../persistence/Services/ProjectionTurns.ts";33import { ProjectionTurnRepository } from "../../persistence/Services/ProjectionTurns.ts";
...@@ -754,23 +753,15 @@ export function runtimeEventToActivities(...@@ -754,23 +753,15 @@ export function runtimeEventToActivities(
754 return [];753 return [];
755 }754 }
756755
757 const beforeTokens = event.payload.beforeTokens;
758 const afterTokens = event.payload.afterTokens;
759 const summary =
760 beforeTokens !== undefined && afterTokens !== undefined
761 ? `Compacted context ${formatTokens(beforeTokens)} → ${formatTokens(afterTokens)} tokens`
762 : "Context compacted";
763 return [756 return [
764 {757 {
765 id: event.eventId,758 id: event.eventId,
766 createdAt: event.createdAt,759 createdAt: event.createdAt,
767 tone: "info",760 tone: "info",
768 kind: "context-compaction",761 kind: "context-compaction",
769 summary,762 summary: "Context compacted",
770 payload: {763 payload: {
771 state: event.payload.state,764 state: event.payload.state,
772 ...(beforeTokens !== undefined ? { beforeTokens } : {}),
773 ...(afterTokens !== undefined ? { afterTokens } : {}),
774 ...(event.payload.detail !== undefined ? { detail: event.payload.detail } : {}),765 ...(event.payload.detail !== undefined ? { detail: event.payload.detail } : {}),
775 },766 },
776 turnId: toTurnId(event.turnId) ?? null,767 turnId: toTurnId(event.turnId) ?? null,
apps/server/src/provider/Layers/ClaudeAdapter.test.ts+6-7
...@@ -729,7 +729,12 @@ describe("ClaudeAdapterLive", () => {...@@ -729,7 +729,12 @@ describe("ClaudeAdapterLive", () => {
729 runtimeMode: "full-access",729 runtimeMode: "full-access",
730 });730 });
731731
732 yield* adapter.compactThread!(session.threadId, modelSelection);732 yield* adapter.sendTurn({
733 threadId: session.threadId,
734 input: "/compact",
735 attachments: [],
736 modelSelection,
737 });
733738
734 const promptText = yield* Effect.promise(() =>739 const promptText = yield* Effect.promise(() =>
735 readFirstPromptText(harness.getLastCreateQueryInput()),740 readFirstPromptText(harness.getLastCreateQueryInput()),
...@@ -2227,12 +2232,6 @@ describe("ClaudeAdapterLive", () => {...@@ -2227,12 +2232,6 @@ describe("ClaudeAdapterLive", () => {
2227 } as unknown as SDKMessage);2232 } as unknown as SDKMessage);
22282233
2229 const runtimeEvents = Array.from(yield* Fiber.join(runtimeEventsFiber));2234 const runtimeEvents = Array.from(yield* Fiber.join(runtimeEventsFiber));
2230 const compactionEvent = runtimeEvents.find(
2231 (event) => event.type === "thread.state.changed" && event.payload.state === "compacted",
2232 );
2233 assert.ok(compactionEvent?.type === "thread.state.changed");
2234 assert.equal(compactionEvent.payload.beforeTokens, 200);
2235 assert.equal(compactionEvent.payload.afterTokens, 40);
2236 const finalUsageEvent = runtimeEvents.findLast(2235 const finalUsageEvent = runtimeEvents.findLast(
2237 (event) => event.type === "thread.token-usage.updated",2236 (event) => event.type === "thread.token-usage.updated",
2238 );2237 );
apps/server/src/provider/Layers/ClaudeAdapter.ts+13-28
...@@ -3198,36 +3198,32 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (...@@ -3198,36 +3198,32 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (
3198 },3198 },
3199 });3199 });
3200 return;3200 return;
3201 case "compact_boundary": {3201 case "compact_boundary":
3202 if (context.turnState) {3202 if (context.turnState) {
3203 context.turnState.latestAssistantUsage = undefined;3203 context.turnState.latestAssistantUsage = undefined;
3204 context.turnState.compactedSinceLatestAssistantUsage = true;3204 context.turnState.compactedSinceLatestAssistantUsage = true;
3205 }3205 }
3206 const compactedUsage = compactBoundaryTokenUsageSnapshot(3206 yield* emitThreadTokenUsage(
3207 message as unknown as Record<string, unknown>,3207 context,
3208 context.lastKnownContextWindow,3208 compactBoundaryTokenUsageSnapshot(
3209 context.lastKnownTotalProcessedTokens,3209 message as unknown as Record<string, unknown>,
3210 context.lastKnownContextWindow,
3211 context.lastKnownTotalProcessedTokens,
3212 ),
3213 {
3214 rawMethod: "claude/system/compact_boundary",
3215 rawPayload: message,
3216 },
3210 );3217 );
3211 yield* emitThreadTokenUsage(context, compactedUsage, {
3212 rawMethod: "claude/system/compact_boundary",
3213 rawPayload: message,
3214 });
3215 yield* offerRuntimeEvent({3218 yield* offerRuntimeEvent({
3216 ...base,3219 ...base,
3217 type: "thread.state.changed",3220 type: "thread.state.changed",
3218 payload: {3221 payload: {
3219 state: "compacted",3222 state: "compacted",
3220 ...(compactedUsage?.lastUsedTokens !== undefined
3221 ? { beforeTokens: compactedUsage.lastUsedTokens }
3222 : {}),
3223 ...(compactedUsage?.usedTokens !== undefined
3224 ? { afterTokens: compactedUsage.usedTokens }
3225 : {}),
3226 detail: message,3223 detail: message,
3227 },3224 },
3228 });3225 });
3229 return;3226 return;
3230 }
3231 case "hook_started":3227 case "hook_started":
3232 yield* offerRuntimeEvent({3228 yield* offerRuntimeEvent({
3233 ...base,3229 ...base,
...@@ -3786,7 +3782,7 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (...@@ -3786,7 +3782,7 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (
37863782
3787 // Same reason as the approvals above: a request nobody can answer any more3783 // Same reason as the approvals above: a request nobody can answer any more
3788 // must not stay open, or the thread can never be settled.3784 // must not stay open, or the thread can never be settled.
3789 for (const pending of context.pendingUserInputs.values()) {3785 for (const pending of [...context.pendingUserInputs.values()]) {
3790 yield* pending.cancel;3786 yield* pending.cancel;
3791 }3787 }
37923788
...@@ -4694,16 +4690,6 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (...@@ -4694,16 +4690,6 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (
4694 };4690 };
4695 });4691 });
46964692
4697 const compactThread: NonNullable<ClaudeAdapterShape["compactThread"]> = (
4698 threadId,
4699 modelSelection,
4700 ) =>
4701 sendTurn({
4702 threadId,
4703 input: "/compact",
4704 ...(modelSelection !== undefined ? { modelSelection } : {}),
4705 }).pipe(Effect.asVoid);
4706
4707 const interruptTurn: ClaudeAdapterShape["interruptTurn"] = Effect.fn("interruptTurn")(4693 const interruptTurn: ClaudeAdapterShape["interruptTurn"] = Effect.fn("interruptTurn")(
4708 function* (threadId, _turnId) {4694 function* (threadId, _turnId) {
4709 const context = yield* requireSession(threadId);4695 const context = yield* requireSession(threadId);
...@@ -4818,7 +4804,6 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (...@@ -4818,7 +4804,6 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* (
4818 },4804 },
4819 startSession,4805 startSession,
4820 sendTurn,4806 sendTurn,
4821 compactThread,
4822 interruptTurn,4807 interruptTurn,
4823 readThread,4808 readThread,
4824 rollbackThread,4809 rollbackThread,
apps/server/src/provider/Layers/ClaudeProvider.ts+7-2
...@@ -22,7 +22,6 @@ import {...@@ -22,7 +22,6 @@ import {
2222
23import {23import {
24 buildServerProvider,24 buildServerProvider,
25 COMPACT_SLASH_COMMAND,
26 DEFAULT_TIMEOUT_MS,25 DEFAULT_TIMEOUT_MS,
27 isCommandMissingCause,26 isCommandMissingCause,
28 parseGenericCliVersion,27 parseGenericCliVersion,
...@@ -504,7 +503,13 @@ export const checkClaudeProviderStatus = Effect.fn("checkClaudeProviderStatus")(...@@ -504,7 +503,13 @@ export const checkClaudeProviderStatus = Effect.fn("checkClaudeProviderStatus")(
504 ? yield* resolveCapabilities(claudeSettings).pipe(Effect.orElseSucceed(() => undefined))503 ? yield* resolveCapabilities(claudeSettings).pipe(Effect.orElseSucceed(() => undefined))
505 : undefined;504 : undefined;
506 const skills = yield* discoverClaudeSkills(claudeSettings, cwd, resolvedEnvironment);505 const skills = yield* discoverClaudeSkills(claudeSettings, cwd, resolvedEnvironment);
507 const slashCommands = [COMPACT_SLASH_COMMAND, ...(capabilities?.slashCommands ?? [])];506 const slashCommands = [
507 {
508 name: "compact",
509 description: "Summarize the conversation and reduce context usage",
510 },
511 ...(capabilities?.slashCommands ?? []),
512 ];
508 const dedupedSlashCommands = dedupeSlashCommands(slashCommands);513 const dedupedSlashCommands = dedupeSlashCommands(slashCommands);
509514
510 if (!capabilities) {515 if (!capabilities) {
apps/server/src/provider/Layers/CodexAdapter.test.ts-43
...@@ -84,8 +84,6 @@ class FakeCodexRuntime implements CodexSessionRuntimeShape {...@@ -84,8 +84,6 @@ class FakeCodexRuntime implements CodexSessionRuntimeShape {
84 }),84 }),
85 );85 );
8686
87 public readonly compactThread = Effect.void;
88
89 public readonly interruptTurnImpl = vi.fn(87 public readonly interruptTurnImpl = vi.fn(
90 (_turnId?: TurnId): Promise<void> => Promise.resolve(undefined),88 (_turnId?: TurnId): Promise<void> => Promise.resolve(undefined),
91 );89 );
...@@ -338,47 +336,6 @@ sessionErrorLayer("CodexAdapterLive session errors", (it) => {...@@ -338,47 +336,6 @@ sessionErrorLayer("CodexAdapterLive session errors", (it) => {
338 }),336 }),
339 );337 );
340338
341 it.effect("compacts the active Codex thread and emits compacted state", () =>
342 Effect.gen(function* () {
343 const adapter = yield* CodexAdapter;
344 const threadId = asThreadId("thread-compact");
345 yield* adapter.startSession({
346 provider: ProviderDriverKind.make("codex"),
347 threadId,
348 runtimeMode: "full-access",
349 });
350 const runtime = sessionRuntimeFactory.lastRuntime;
351 NodeAssert.ok(runtime);
352 const compactedEventFiber = yield* adapter.streamEvents.pipe(
353 Stream.filter((event) => event.type === "thread.state.changed"),
354 Stream.runHead,
355 Effect.forkChild,
356 );
357 yield* adapter.compactThread!(threadId);
358 yield* runtime.emit({
359 id: asEventId("evt-compaction-item-completed"),
360 kind: "notification",
361 provider: ProviderDriverKind.make("codex"),
362 createdAt: "2026-01-01T00:00:00.000Z",
363 method: "item/completed",
364 threadId,
365 payload: {
366 completedAtMs: 1_778_000_000_000,
367 threadId: "provider-thread-1",
368 turnId: "provider-compact-turn",
369 item: {
370 id: "provider-compact-item",
371 type: "contextCompaction",
372 },
373 },
374 });
375 const event = Option.getOrThrow(yield* Fiber.join(compactedEventFiber));
376 NodeAssert.ok(event.type === "thread.state.changed");
377 NodeAssert.equal(event.payload.state, "compacted");
378 yield* adapter.stopSession(threadId);
379 }),
380 );
381
382 it.effect("uploads feedback for the active Codex thread", () =>339 it.effect("uploads feedback for the active Codex thread", () =>
383 Effect.gen(function* () {340 Effect.gen(function* () {
384 const adapter = yield* CodexAdapter;341 const adapter = yield* CodexAdapter;
apps/server/src/provider/Layers/CodexAdapter.ts+1-23
...@@ -11,7 +11,6 @@ import {...@@ -11,7 +11,6 @@ import {
11 type CanonicalItemType,11 type CanonicalItemType,
12 type CanonicalRequestType,12 type CanonicalRequestType,
13 type CodexSettings,13 type CodexSettings,
14 EventId,
15 ProviderDriverKind,14 ProviderDriverKind,
16 type ProviderEvent,15 type ProviderEvent,
17 ProviderInstanceId,16 ProviderInstanceId,
...@@ -1152,18 +1151,7 @@ function mapToRuntimeEvents(...@@ -1152,18 +1151,7 @@ function mapToRuntimeEvents(
1152 ];1151 ];
1153 }1152 }
1154 const completed = mapItemLifecycle(event, canonicalThreadId, "item.completed");1153 const completed = mapItemLifecycle(event, canonicalThreadId, "item.completed");
1155 if (!completed || itemType !== "context_compaction") {1154 return completed ? [completed] : [];
1156 return completed ? [completed] : [];
1157 }
1158 return [
1159 completed,
1160 {
1161 ...runtimeEventBase(event, canonicalThreadId),
1162 eventId: EventId.make(`${event.id}:thread-compacted`),
1163 type: "thread.state.changed",
1164 payload: { state: "compacted" },
1165 },
1166 ];
1167 }1155 }
11681156
1169 if (1157 if (
...@@ -1891,15 +1879,6 @@ export const makeCodexAdapter = Effect.fn("makeCodexAdapter")(function* (...@@ -1891,15 +1879,6 @@ export const makeCodexAdapter = Effect.fn("makeCodexAdapter")(function* (
1891 ),1879 ),
1892 );1880 );
18931881
1894 const compactThread: NonNullable<CodexAdapterShape["compactThread"]> = Effect.fn("compactThread")(
1895 function* (threadId) {
1896 const session = yield* requireSession(threadId);
1897 yield* session.runtime.compactThread.pipe(
1898 Effect.mapError((cause) => mapCodexRuntimeError(threadId, "thread/compact/start", cause)),
1899 );
1900 },
1901 );
1902
1903 const readThread: CodexAdapterShape["readThread"] = (threadId) =>1882 const readThread: CodexAdapterShape["readThread"] = (threadId) =>
1904 requireSession(threadId).pipe(1883 requireSession(threadId).pipe(
1905 Effect.flatMap((session) => session.runtime.readThread),1884 Effect.flatMap((session) => session.runtime.readThread),
...@@ -2035,7 +2014,6 @@ export const makeCodexAdapter = Effect.fn("makeCodexAdapter")(function* (...@@ -2035,7 +2014,6 @@ export const makeCodexAdapter = Effect.fn("makeCodexAdapter")(function* (
2035 },2014 },
2036 startSession,2015 startSession,
2037 sendTurn,2016 sendTurn,
2038 compactThread,
2039 interruptTurn,2017 interruptTurn,
2040 readThread,2018 readThread,
2041 rollbackThread,2019 rollbackThread,
apps/server/src/provider/Layers/CodexProvider.ts-2
...@@ -30,7 +30,6 @@ import { codexAppServerArgs, resolveCodexLaunchArgs } from "./codexLaunchArgs.ts...@@ -30,7 +30,6 @@ import { codexAppServerArgs, resolveCodexLaunchArgs } from "./codexLaunchArgs.ts
30import {30import {
31 AUTH_PROBE_TIMEOUT_MS,31 AUTH_PROBE_TIMEOUT_MS,
32 buildServerProvider,32 buildServerProvider,
33 COMPACT_SLASH_COMMAND,
34 type ServerProviderDraft,33 type ServerProviderDraft,
35} from "../providerSnapshot.ts";34} from "../providerSnapshot.ts";
36import { expandHomePath } from "../../pathExpansion.ts";35import { expandHomePath } from "../../pathExpansion.ts";
...@@ -649,7 +648,6 @@ export const checkCodexProviderStatus = Effect.fn("checkCodexProviderStatus")(fu...@@ -649,7 +648,6 @@ export const checkCodexProviderStatus = Effect.fn("checkCodexProviderStatus")(fu
649 models: snapshot.models,648 models: snapshot.models,
650 skills: snapshot.skills,649 skills: snapshot.skills,
651 slashCommands: [650 slashCommands: [
652 COMPACT_SLASH_COMMAND,
653 {651 {
654 name: "feedback",652 name: "feedback",
655 description: "Send this thread and Codex logs to OpenAI",653 description: "Send this thread and Codex logs to OpenAI",
apps/server/src/provider/Layers/CodexSessionRuntime.ts-5
...@@ -196,7 +196,6 @@ export interface CodexSessionRuntimeShape {...@@ -196,7 +196,6 @@ export interface CodexSessionRuntimeShape {
196 readonly sendTurn: (196 readonly sendTurn: (
197 input: CodexSessionRuntimeSendTurnInput,197 input: CodexSessionRuntimeSendTurnInput,
198 ) => Effect.Effect<ProviderTurnStartResult, CodexSessionRuntimeError>;198 ) => Effect.Effect<ProviderTurnStartResult, CodexSessionRuntimeError>;
199 readonly compactThread: Effect.Effect<void, CodexSessionRuntimeError>;
200 readonly interruptTurn: (turnId?: TurnId) => Effect.Effect<void, CodexSessionRuntimeError>;199 readonly interruptTurn: (turnId?: TurnId) => Effect.Effect<void, CodexSessionRuntimeError>;
201 readonly readThread: Effect.Effect<CodexThreadSnapshot, CodexSessionRuntimeError>;200 readonly readThread: Effect.Effect<CodexThreadSnapshot, CodexSessionRuntimeError>;
202 readonly rollbackThread: (201 readonly rollbackThread: (
...@@ -2294,10 +2293,6 @@ export const makeCodexSessionRuntime = (...@@ -2294,10 +2293,6 @@ export const makeCodexSessionRuntime = (
2294 return {2293 return {
2295 start,2294 start,
2296 getSession: Ref.get(sessionRef),2295 getSession: Ref.get(sessionRef),
2297 compactThread: Effect.gen(function* () {
2298 const providerThreadId = yield* readProviderThreadId;
2299 yield* client.request("thread/compact/start", { threadId: providerThreadId });
2300 }),
2301 sendTurn: (input) =>2296 sendTurn: (input) =>
2302 Effect.gen(function* () {2297 Effect.gen(function* () {
2303 const providerThreadId = yield* readProviderThreadId;2298 const providerThreadId = yield* readProviderThreadId;
apps/server/src/provider/Layers/CursorProvider.ts-2
...@@ -34,7 +34,6 @@ import {...@@ -34,7 +34,6 @@ import {
34 buildBooleanOptionDescriptor,34 buildBooleanOptionDescriptor,
35 buildSelectOptionDescriptor,35 buildSelectOptionDescriptor,
36 buildServerProvider,36 buildServerProvider,
37 COMPACT_SLASH_COMMAND,
38 collectStreamAsString,37 collectStreamAsString,
39 isCommandMissingCause,38 isCommandMissingCause,
40 providerModelsFromSettings,39 providerModelsFromSettings,
...@@ -640,7 +639,6 @@ export function buildCursorProviderSnapshot(input: {...@@ -640,7 +639,6 @@ export function buildCursorProviderSnapshot(input: {
640 input.cursorSettings.customModels,639 input.cursorSettings.customModels,
641 EMPTY_CAPABILITIES,640 EMPTY_CAPABILITIES,
642 ),641 ),
643 slashCommands: [COMPACT_SLASH_COMMAND],
644 probe: {642 probe: {
645 installed: true,643 installed: true,
646 version: input.parsed.version,644 version: input.parsed.version,
apps/server/src/provider/Layers/GrokProvider.ts-2
...@@ -21,7 +21,6 @@ import { resolveSpawnCommand } from "@t3tools/shared/shell";...@@ -21,7 +21,6 @@ import { resolveSpawnCommand } from "@t3tools/shared/shell";
21import {21import {
22 AUTH_PROBE_TIMEOUT_MS,22 AUTH_PROBE_TIMEOUT_MS,
23 buildServerProvider,23 buildServerProvider,
24 COMPACT_SLASH_COMMAND,
25 isCommandMissingCause,24 isCommandMissingCause,
26 parseGenericCliVersion,25 parseGenericCliVersion,
27 providerModelsFromSettings,26 providerModelsFromSettings,
...@@ -497,7 +496,6 @@ export const checkGrokProviderStatus = Effect.fn("checkGrokProviderStatus")(func...@@ -497,7 +496,6 @@ export const checkGrokProviderStatus = Effect.fn("checkGrokProviderStatus")(func
497 checkedAt,496 checkedAt,
498 models,497 models,
499 skills,498 skills,
500 slashCommands: [COMPACT_SLASH_COMMAND],
501 probe: {499 probe: {
502 installed: true,500 installed: true,
503 version,501 version,
apps/server/src/provider/Layers/OpenCodeAdapter.test.ts-39
...@@ -79,7 +79,6 @@ const runtimeMock = {...@@ -79,7 +79,6 @@ const runtimeMock = {
79 messageCalls: [] as Array<{ sessionID: string; messageID: string }>,79 messageCalls: [] as Array<{ sessionID: string; messageID: string }>,
80 messageFailures: 0,80 messageFailures: 0,
81 promptCalls: [] as Array<unknown>,81 promptCalls: [] as Array<unknown>,
82 summarizeCalls: [] as Array<unknown>,
83 promptAsyncError: null as Error | null,82 promptAsyncError: null as Error | null,
84 promptAsyncImplementation: null as (() => Promise<void>) | null,83 promptAsyncImplementation: null as (() => Promise<void>) | null,
85 autoPromptEcho: true,84 autoPromptEcho: true,
...@@ -130,7 +129,6 @@ const runtimeMock = {...@@ -130,7 +129,6 @@ const runtimeMock = {
130 this.state.messageCalls.length = 0;129 this.state.messageCalls.length = 0;
131 this.state.messageFailures = 0;130 this.state.messageFailures = 0;
132 this.state.promptCalls.length = 0;131 this.state.promptCalls.length = 0;
133 this.state.summarizeCalls.length = 0;
134 this.state.promptAsyncError = null;132 this.state.promptAsyncError = null;
135 this.state.promptAsyncImplementation = null;133 this.state.promptAsyncImplementation = null;
136 this.state.autoPromptEcho = true;134 this.state.autoPromptEcho = true;
...@@ -315,10 +313,6 @@ const OpenCodeRuntimeTestDouble: OpenCodeRuntimeShape = {...@@ -315,10 +313,6 @@ const OpenCodeRuntimeTestDouble: OpenCodeRuntimeShape = {
315 });313 });
316 }314 }
317 },315 },
318 summarize: async (input: unknown) => {
319 runtimeMock.state.summarizeCalls.push(input);
320 return { data: true };
321 },
322 messages: async () => ({ data: runtimeMock.state.messages }),316 messages: async () => ({ data: runtimeMock.state.messages }),
323 message: async ({ sessionID, messageID }: { sessionID: string; messageID: string }) => {317 message: async ({ sessionID, messageID }: { sessionID: string; messageID: string }) => {
324 runtimeMock.state.messageCalls.push({ sessionID, messageID });318 runtimeMock.state.messageCalls.push({ sessionID, messageID });
...@@ -955,39 +949,6 @@ it.layer(OpenCodeAdapterTestLayer)("OpenCodeAdapterLive", (it) => {...@@ -955,39 +949,6 @@ it.layer(OpenCodeAdapterTestLayer)("OpenCodeAdapterLive", (it) => {
955 }),949 }),
956 );950 );
957951
958 it.effect("compacts through the native OpenCode session API", () =>
959 Effect.gen(function* () {
960 const adapter = yield* OpenCodeAdapter;
961 const threadId = asThreadId("thread-opencode-compact");
962 runtimeMock.state.subscribedEvents.push({
963 type: "session.compacted",
964 properties: { sessionID: "http://127.0.0.1:9999/session" },
965 });
966 const eventsFiber = yield* adapter.streamEvents.pipe(
967 Stream.filter((event) => event.threadId === threadId),
968 Stream.take(3),
969 Stream.runCollect,
970 Effect.forkChild,
971 );
972 yield* adapter.startSession({
973 provider: ProviderDriverKind.make("opencode"),
974 threadId,
975 runtimeMode: "full-access",
976 });
977 yield* adapter.compactThread!(
978 threadId,
979 createModelSelection(ProviderInstanceId.make("opencode"), "openai/gpt-5"),
980 );
981 const summarizeCall = runtimeMock.state.summarizeCalls[0] as Record<string, unknown>;
982 NodeAssert.equal(summarizeCall.modelID, "gpt-5");
983 const events = Array.from(yield* Fiber.join(eventsFiber));
984 yield* adapter.stopSession(threadId);
985 const compacted = events.some(
986 (event) => event.type === "thread.state.changed" && event.payload.state === "compacted",
987 );
988 NodeAssert.equal(compacted, true);
989 }),
990 );
991 it.effect("falls back to a fresh session when the persisted session is gone", () =>952 it.effect("falls back to a fresh session when the persisted session is gone", () =>
992 Effect.gen(function* () {953 Effect.gen(function* () {
993 const adapter = yield* OpenCodeAdapter;954 const adapter = yield* OpenCodeAdapter;
apps/server/src/provider/Layers/OpenCodeAdapter.ts-81
...@@ -2014,21 +2014,6 @@ export function makeOpenCodeAdapter(...@@ -2014,21 +2014,6 @@ export function makeOpenCodeAdapter(
2014 }2014 }
2015 break;2015 break;
2016 }2016 }
2017 case "session.compacted": {
2018 yield* emit({
2019 ...(yield* buildEventBase({
2020 threadId: context.session.threadId,
2021 turnId,
2022 raw: event,
2023 })),
2024 type: "thread.state.changed",
2025 payload: {
2026 state: "compacted",
2027 detail: event,
2028 },
2029 });
2030 break;
2031 }
20322017
2033 case "message.updated": {2018 case "message.updated": {
2034 const promptAdmission = context.promptAdmission;2019 const promptAdmission = context.promptAdmission;
...@@ -2999,71 +2984,6 @@ export function makeOpenCodeAdapter(...@@ -2999,71 +2984,6 @@ export function makeOpenCodeAdapter(
2999 );2984 );
3000 });2985 });
30012986
3002 const compactThread: NonNullable<OpenCodeAdapterShape["compactThread"]> = Effect.fn(
3003 "compactThread",
3004 )(function* (threadId, requestedModelSelection) {
3005 const context = yield* ensureSessionContext(sessions, threadId);
3006 yield* awaitOpenCodeContextReady(context);
3007 const modelSelection =
3008 requestedModelSelection ??
3009 (context.session.model
3010 ? { instanceId: boundInstanceId, model: context.session.model }
3011 : undefined);
3012 if (modelSelection !== undefined && modelSelection.instanceId !== boundInstanceId) {
3013 return yield* new ProviderAdapterValidationError({
3014 provider: PROVIDER,
3015 operation: "compactThread",
3016 issue: `OpenCode model selection is bound to instance '${modelSelection.instanceId}', expected '${boundInstanceId}'.`,
3017 });
3018 }
3019 const parsedModel = parseOpenCodeModelSlug(modelSelection?.model);
3020 if (!parsedModel) {
3021 return yield* new ProviderAdapterValidationError({
3022 provider: PROVIDER,
3023 operation: "compactThread",
3024 issue: "OpenCode compaction requires an active 'provider/model' selection.",
3025 });
3026 }
3027 yield* context.promptSemaphore.withPermit(
3028 Effect.gen(function* () {
3029 if (sessions.get(threadId) !== context || (yield* Ref.get(context.stopped))) {
3030 return yield* Effect.interrupt;
3031 }
3032 if (context.activeTurnId !== undefined) {
3033 return yield* new ProviderAdapterValidationError({
3034 provider: PROVIDER,
3035 operation: "compactThread",
3036 issue: "OpenCode cannot compact while a turn is running.",
3037 });
3038 }
3039 yield* runOpenCodeSdk("session.summarize", (signal) =>
3040 context.client.session.summarize(
3041 {
3042 sessionID: context.openCodeSessionId,
3043 ...parsedModel,
3044 auto: false,
3045 },
3046 { signal },
3047 ),
3048 ).pipe(
3049 Effect.timeout("10 minutes"),
3050 Effect.catchTags({
3051 OpenCodeRuntimeError: (cause) => Effect.fail(toRequestError(cause)),
3052 TimeoutError: (cause) =>
3053 Effect.fail(
3054 new ProviderAdapterRequestError({
3055 provider: PROVIDER,
3056 method: "session.summarize",
3057 detail: "OpenCode session compaction did not complete within 10 minutes.",
3058 cause,
3059 }),
3060 ),
3061 }),
3062 Effect.asVoid,
3063 );
3064 }),
3065 );
3066 });
3067 const interruptTurn: OpenCodeAdapterShape["interruptTurn"] = Effect.fn("interruptTurn")(2987 const interruptTurn: OpenCodeAdapterShape["interruptTurn"] = Effect.fn("interruptTurn")(
3068 function* (threadId, turnId) {2988 function* (threadId, turnId) {
3069 const context = yield* ensureSessionContext(sessions, threadId);2989 const context = yield* ensureSessionContext(sessions, threadId);
...@@ -3348,7 +3268,6 @@ export function makeOpenCodeAdapter(...@@ -3348,7 +3268,6 @@ export function makeOpenCodeAdapter(
3348 },3268 },
3349 startSession,3269 startSession,
3350 sendTurn,3270 sendTurn,
3351 compactThread,
3352 interruptTurn,3271 interruptTurn,
3353 respondToRequest,3272 respondToRequest,
3354 respondToUserInput,3273 respondToUserInput,
apps/server/src/provider/Layers/OpenCodeProvider.ts-2
...@@ -13,7 +13,6 @@ import { createModelCapabilities } from "@t3tools/shared/model";...@@ -13,7 +13,6 @@ import { createModelCapabilities } from "@t3tools/shared/model";
13import { compareSemverVersions } from "@t3tools/shared/semver";13import { compareSemverVersions } from "@t3tools/shared/semver";
14import {14import {
15 buildServerProvider,15 buildServerProvider,
16 COMPACT_SLASH_COMMAND,
17 nonEmptyTrimmed,16 nonEmptyTrimmed,
18 parseGenericCliVersion,17 parseGenericCliVersion,
19 providerModelsFromSettings,18 providerModelsFromSettings,
...@@ -496,7 +495,6 @@ export const checkOpenCodeProviderStatus = Effect.fn("checkOpenCodeProviderStatu...@@ -496,7 +495,6 @@ export const checkOpenCodeProviderStatus = Effect.fn("checkOpenCodeProviderStatu
496 checkedAt,495 checkedAt,
497 models,496 models,
498 skills,497 skills,
499 slashCommands: [COMPACT_SLASH_COMMAND],
500 probe: {498 probe: {
501 installed: true,499 installed: true,
502 version,500 version,
apps/server/src/provider/Layers/ProviderRegistry.test.ts+10-4
...@@ -48,7 +48,6 @@ import {...@@ -48,7 +48,6 @@ import {
48import * as ServerConfig from "../../config.ts";48import * as ServerConfig from "../../config.ts";
49import * as ServerSettingsModule from "../../serverSettings.ts";49import * as ServerSettingsModule from "../../serverSettings.ts";
50import { readProviderStatusCache, resolveProviderStatusCachePath } from "../providerStatusCache.ts";50import { readProviderStatusCache, resolveProviderStatusCachePath } from "../providerStatusCache.ts";
51import { COMPACT_SLASH_COMMAND } from "../providerSnapshot.ts";
52import type { ProviderInstance } from "../ProviderDriver.ts";51import type { ProviderInstance } from "../ProviderDriver.ts";
53import * as ProviderInstanceRegistry from "../Services/ProviderInstanceRegistry.ts";52import * as ProviderInstanceRegistry from "../Services/ProviderInstanceRegistry.ts";
54import * as ProviderRegistry from "../Services/ProviderRegistry.ts";53import * as ProviderRegistry from "../Services/ProviderRegistry.ts";
...@@ -385,7 +384,7 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsModule.layerTest(), Te...@@ -385,7 +384,7 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsModule.layerTest(), Te
385 shortDescription: "Debug failing GitHub Actions checks",384 shortDescription: "Debug failing GitHub Actions checks",
386 },385 },
387 ]);386 ]);
388 assert.deepStrictEqual(status.slashCommands.slice(1), [387 assert.deepStrictEqual(status.slashCommands, [
389 {388 {
390 name: "feedback",389 name: "feedback",
391 description: "Send this thread and Codex logs to OpenAI",390 description: "Send this thread and Codex logs to OpenAI",
...@@ -2440,7 +2439,11 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsModule.layerTest(), Te...@@ -2440,7 +2439,11 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsModule.layerTest(), Te
2440 }),2439 }),
2441 );2440 );
24422441
2443 assert.deepStrictEqual(status.slashCommands.slice(1), [2442 assert.deepStrictEqual(status.slashCommands, [
2443 {
2444 name: "compact",
2445 description: "Summarize the conversation and reduce context usage",
2446 },
2444 {2447 {
2445 name: "review",2448 name: "review",
2446 description: "Review a pull request",2449 description: "Review a pull request",
...@@ -2484,7 +2487,10 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsModule.layerTest(), Te...@@ -2484,7 +2487,10 @@ it.layer(Layer.mergeAll(NodeServices.layer, ServerSettingsModule.layerTest(), Te
2484 );2487 );
24852488
2486 assert.deepStrictEqual(status.slashCommands, [2489 assert.deepStrictEqual(status.slashCommands, [
2487 COMPACT_SLASH_COMMAND,2490 {
2491 name: "compact",
2492 description: "Summarize the conversation and reduce context usage",
2493 },
2488 {2494 {
2489 name: "ui",2495 name: "ui",
2490 description: "Explore and refine UI",2496 description: "Explore and refine UI",
apps/server/src/provider/Layers/ProviderService.test.ts-52
...@@ -172,18 +172,6 @@ function makeFakeCodexAdapter(provider: ProviderDriverKind = CODEX_DRIVER) {...@@ -172,18 +172,6 @@ function makeFakeCodexAdapter(provider: ProviderDriverKind = CODEX_DRIVER) {
172 Effect.void,172 Effect.void,
173 );173 );
174174
175 const compactThread = vi.fn((threadId: ThreadId) =>
176 Effect.sync(() =>
177 emit({
178 type: "thread.state.changed",
179 eventId: asEventId("evt-native-compact"),
180 provider,
181 createdAt: "2026-01-01T00:00:00.000Z",
182 threadId,
183 payload: { state: "compacted" },
184 }),
185 ),
186 );
187 const respondToRequest = vi.fn(175 const respondToRequest = vi.fn(
188 (176 (
189 _threadId: ThreadId,177 _threadId: ThreadId,
...@@ -262,7 +250,6 @@ function makeFakeCodexAdapter(provider: ProviderDriverKind = CODEX_DRIVER) {...@@ -262,7 +250,6 @@ function makeFakeCodexAdapter(provider: ProviderDriverKind = CODEX_DRIVER) {
262 },250 },
263 startSession,251 startSession,
264 sendTurn,252 sendTurn,
265 ...(provider === CODEX_DRIVER ? { compactThread } : {}),
266 interruptTurn,253 interruptTurn,
267 respondToRequest,254 respondToRequest,
268 respondToUserInput,255 respondToUserInput,
...@@ -299,7 +286,6 @@ function makeFakeCodexAdapter(provider: ProviderDriverKind = CODEX_DRIVER) {...@@ -299,7 +286,6 @@ function makeFakeCodexAdapter(provider: ProviderDriverKind = CODEX_DRIVER) {
299 updateSession,286 updateSession,
300 startSession,287 startSession,
301 sendTurn,288 sendTurn,
302 compactThread,
303 interruptTurn,289 interruptTurn,
304 respondToRequest,290 respondToRequest,
305 respondToUserInput,291 respondToUserInput,
...@@ -1048,10 +1034,6 @@ routing.layer("ProviderServiceLive routing", (it) => {...@@ -1048,10 +1034,6 @@ routing.layer("ProviderServiceLive routing", (it) => {
1048 });1034 });
1049 assert.equal(routing.codex.sendTurn.mock.calls.length, 1);1035 assert.equal(routing.codex.sendTurn.mock.calls.length, 1);
10501036
1051 yield* advanceTestClock(10);
1052 yield* provider.compactThread(session.threadId);
1053 assert.deepEqual(routing.codex.compactThread.mock.calls, [[session.threadId, undefined]]);
1054
1055 yield* provider.interruptTurn({ threadId: session.threadId });1037 yield* provider.interruptTurn({ threadId: session.threadId });
1056 assert.deepEqual(routing.codex.interruptTurn.mock.calls, [[session.threadId, undefined]]);1038 assert.deepEqual(routing.codex.interruptTurn.mock.calls, [[session.threadId, undefined]]);
10571039
...@@ -1115,40 +1097,6 @@ routing.layer("ProviderServiceLive routing", (it) => {...@@ -1115,40 +1097,6 @@ routing.layer("ProviderServiceLive routing", (it) => {
1115 }),1097 }),
1116 );1098 );
11171099
1118 it.effect("marks a successful fallback compaction as compacted", () =>
1119 Effect.gen(function* () {
1120 const provider = yield* ProviderService.ProviderService;
1121 const threadId = asThreadId("thread-compact-cursor");
1122 yield* provider.startSession(threadId, {
1123 provider: CURSOR_DRIVER,
1124 providerInstanceId: ProviderInstanceId.make("cursor"),
1125 threadId,
1126 runtimeMode: "full-access",
1127 });
1128 const compactedEventFiber = yield* provider.streamEvents.pipe(
1129 Stream.filter((event) => event.type === "thread.state.changed"),
1130 Stream.runHead,
1131 Effect.forkChild,
1132 );
1133 const compactFiber = yield* provider.compactThread(threadId).pipe(Effect.forkChild);
1134 yield* advanceTestClock(50);
1135 routing.cursor.emit({
1136 type: "turn.completed",
1137 eventId: asEventId("evt-cursor-compact-completed"),
1138 provider: CURSOR_DRIVER,
1139 createdAt: "2026-01-01T00:00:01.000Z",
1140 threadId,
1141 turnId: asTurnId(`turn-${threadId}`),
1142 payload: { state: "completed" },
1143 });
1144 yield* Fiber.join(compactFiber);
1145
1146 const compacted = yield* Fiber.join(compactedEventFiber);
1147 assert.equal(compacted._tag, "Some");
1148 yield* provider.stopSession({ threadId });
1149 }),
1150 );
1151
1152 it.effect("routes feedback to the Codex adapter and returns its feedback ID", () =>1100 it.effect("routes feedback to the Codex adapter and returns its feedback ID", () =>
1153 Effect.gen(function* () {1101 Effect.gen(function* () {
1154 const provider = yield* ProviderService.ProviderService;1102 const provider = yield* ProviderService.ProviderService;
apps/server/src/provider/Layers/ProviderService.ts+8-111
...@@ -10,7 +10,6 @@...@@ -10,7 +10,6 @@
10 * @module ProviderServiceLive10 * @module ProviderServiceLive
11 */11 */
12import {12import {
13 EventId,
14 ModelSelection,13 ModelSelection,
15 NonNegativeInt,14 NonNegativeInt,
16 ThreadId,15 ThreadId,
...@@ -29,7 +28,6 @@ import {...@@ -29,7 +28,6 @@ import {
29import { expandAssistantCitationsForProvider } from "@t3tools/shared/assistantCitations";28import { expandAssistantCitationsForProvider } from "@t3tools/shared/assistantCitations";
30import { causeErrorTag } from "@t3tools/shared/observability";29import { causeErrorTag } from "@t3tools/shared/observability";
31import * as DateTime from "effect/DateTime";30import * as DateTime from "effect/DateTime";
32import * as Deferred from "effect/Deferred";
33import * as Effect from "effect/Effect";31import * as Effect from "effect/Effect";
34import * as Layer from "effect/Layer";32import * as Layer from "effect/Layer";
35import * as Option from "effect/Option";33import * as Option from "effect/Option";
...@@ -51,7 +49,6 @@ import {...@@ -51,7 +49,6 @@ import {
51 providerTurnMetricAttributes,49 providerTurnMetricAttributes,
52 withMetrics,50 withMetrics,
53} from "../../observability/Metrics.ts";51} from "../../observability/Metrics.ts";
54import { ProviderAdapterRequestError } from "../Errors.ts";
55import { type ProviderAdapterError, ProviderValidationError } from "../Errors.ts";52import { type ProviderAdapterError, ProviderValidationError } from "../Errors.ts";
56import type { ProviderAdapterShape } from "../Services/ProviderAdapter.ts";53import type { ProviderAdapterShape } from "../Services/ProviderAdapter.ts";
57import * as ProviderAdapterRegistry from "../Services/ProviderAdapterRegistry.ts";54import * as ProviderAdapterRegistry from "../Services/ProviderAdapterRegistry.ts";
...@@ -236,17 +233,6 @@ const makeProviderService = Effect.fn("makeProviderService")(function* (...@@ -236,17 +233,6 @@ const makeProviderService = Effect.fn("makeProviderService")(function* (
236 const revokeMcpCredential =233 const revokeMcpCredential =
237 options?.revokeMcpCredential ?? McpSessionRegistry.revokeActiveMcpThread;234 options?.revokeMcpCredential ?? McpSessionRegistry.revokeActiveMcpThread;
238 const runtimeEventPubSub = yield* PubSub.unbounded<ProviderRuntimeEvent>();235 const runtimeEventPubSub = yield* PubSub.unbounded<ProviderRuntimeEvent>();
239 const pendingCompactions = new Map<
240 ThreadId,
241 { readonly completion: Deferred.Deferred<string>; readonly synthesizeCompactedEvent: boolean }
242 >();
243 const settleCompaction = (threadId: ThreadId, terminal: string) => {
244 const pending = pendingCompactions.get(threadId);
245 pendingCompactions.delete(threadId);
246 return pending
247 ? Deferred.succeed(pending.completion, terminal).pipe(Effect.as(pending))
248 : Effect.succeed(undefined);
249 };
250 const nowIso = Effect.map(DateTime.now, DateTime.formatIso);236 const nowIso = Effect.map(DateTime.now, DateTime.formatIso);
251 /**237 /**
252 * Attach the `t3-code` MCP server to the session that is about to start.238 * Attach the `t3-code` MCP server to the session that is about to start.
...@@ -359,58 +345,14 @@ const makeProviderService = Effect.fn("makeProviderService")(function* (...@@ -359,58 +345,14 @@ const makeProviderService = Effect.fn("makeProviderService")(function* (
359 },345 },
360 event: ProviderRuntimeEvent,346 event: ProviderRuntimeEvent,
361 ): Effect.Effect<void> =>347 ): Effect.Effect<void> =>
362 Effect.gen(function* () {348 Effect.sync(() => correlateRuntimeEventWithInstance(source, event)).pipe(
363 const canonicalEvent = yield* Effect.sync(() =>349 Effect.flatMap((canonicalEvent) =>
364 correlateRuntimeEventWithInstance(source, event),350 increment(providerRuntimeEventsTotal, {
365 );351 provider: canonicalEvent.provider,
366 yield* increment(providerRuntimeEventsTotal, {352 eventType: canonicalEvent.type,
367 provider: canonicalEvent.provider,353 }).pipe(Effect.andThen(publishRuntimeEvent(canonicalEvent))),
368 eventType: canonicalEvent.type,354 ),
369 });355 );
370 yield* publishRuntimeEvent(canonicalEvent);
371 const pendingCompaction = pendingCompactions.get(canonicalEvent.threadId);
372 if (!pendingCompaction) return;
373 if (
374 !pendingCompaction.synthesizeCompactedEvent &&
375 canonicalEvent.type === "thread.state.changed" &&
376 canonicalEvent.payload.state === "compacted"
377 ) {
378 yield* settleCompaction(canonicalEvent.threadId, "completed");
379 return;
380 }
381 const compactionTerminal =
382 canonicalEvent.type === "turn.completed"
383 ? canonicalEvent.payload.state
384 : canonicalEvent.type === "session.exited" ||
385 canonicalEvent.type === "runtime.error" ||
386 canonicalEvent.type === "turn.aborted"
387 ? canonicalEvent.type
388 : null;
389 const settledCompaction =
390 compactionTerminal !== null &&
391 (yield* settleCompaction(canonicalEvent.threadId, compactionTerminal));
392 if (
393 !settledCompaction ||
394 compactionTerminal !== "completed" ||
395 !settledCompaction.synthesizeCompactedEvent
396 ) {
397 return;
398 }
399 const compactedEvent = {
400 ...canonicalEvent,
401 eventId: EventId.make(`${canonicalEvent.eventId}:context-compaction`),
402 type: "thread.state.changed",
403 payload: {
404 state: "compacted",
405 detail: { source: "provider-native-command" },
406 },
407 } satisfies ProviderRuntimeEvent;
408 yield* increment(providerRuntimeEventsTotal, {
409 provider: compactedEvent.provider,
410 eventType: compactedEvent.type,
411 });
412 yield* publishRuntimeEvent(compactedEvent);
413 });
414356
415 // `subscribedAdapters` is our source-of-truth for "which instance adapters357 // `subscribedAdapters` is our source-of-truth for "which instance adapters
416 // are currently wired into the runtime event bus". It both tracks the set358 // are currently wired into the runtime event bus". It both tracks the set
...@@ -912,50 +854,6 @@ const makeProviderService = Effect.fn("makeProviderService")(function* (...@@ -912,50 +854,6 @@ const makeProviderService = Effect.fn("makeProviderService")(function* (
912 );854 );
913 });855 });
914856
915 const compactThread: ProviderServiceMethod<"compactThread"> = Effect.fn("compactThread")(
916 function* (threadId, modelSelection) {
917 const routed = yield* resolveRoutableSession({
918 threadId,
919 operation: "ProviderService.compactThread",
920 allowRecovery: true,
921 });
922 yield* Effect.annotateCurrentSpan({
923 "provider.operation": "compact-thread",
924 "provider.kind": routed.adapter.provider,
925 "provider.thread_id": threadId,
926 });
927 yield* McpSessionRegistry.touchActiveMcpThread(threadId);
928 const nativeCompaction = routed.adapter.compactThread;
929 const completion = yield* Deferred.make<string>();
930 pendingCompactions.set(threadId, {
931 completion,
932 synthesizeCompactedEvent: nativeCompaction === undefined,
933 });
934 const terminal = yield* (
935 nativeCompaction
936 ? nativeCompaction(routed.threadId, modelSelection)
937 : sendTurn({
938 threadId,
939 input: routed.adapter.provider === "cursor" ? "/compress" : "/compact",
940 ...(modelSelection !== undefined ? { modelSelection } : {}),
941 })
942 ).pipe(
943 Effect.andThen(Deferred.await(completion)),
944 Effect.ensuring(Effect.sync(() => void pendingCompactions.delete(threadId))),
945 );
946 if (terminal !== "completed") {
947 return yield* new ProviderAdapterRequestError({
948 provider: routed.adapter.provider,
949 method: "turn/start",
950 detail: `Context compaction ended with ${terminal}.`,
951 });
952 }
953 yield* analytics.record("provider.thread.compacted", {
954 provider: routed.adapter.provider,
955 });
956 },
957 );
958
959 const interruptTurn: ProviderServiceMethod<"interruptTurn"> = Effect.fn("interruptTurn")(857 const interruptTurn: ProviderServiceMethod<"interruptTurn"> = Effect.fn("interruptTurn")(
960 function* (rawInput) {858 function* (rawInput) {
961 const input = yield* decodeInputOrValidationError({859 const input = yield* decodeInputOrValidationError({
...@@ -1351,7 +1249,6 @@ const makeProviderService = Effect.fn("makeProviderService")(function* (...@@ -1351,7 +1249,6 @@ const makeProviderService = Effect.fn("makeProviderService")(function* (
1351 return {1249 return {
1352 startSession,1250 startSession,
1353 sendTurn,1251 sendTurn,
1354 compactThread,
1355 interruptTurn,1252 interruptTurn,
1356 respondToRequest,1253 respondToRequest,
1357 respondToUserInput,1254 respondToUserInput,
apps/server/src/provider/Layers/ProviderSessionReaper.test.ts-1
...@@ -164,7 +164,6 @@ describe("ProviderSessionReaper", () => {...@@ -164,7 +164,6 @@ describe("ProviderSessionReaper", () => {
164 const providerService: ProviderServiceShape = {164 const providerService: ProviderServiceShape = {
165 startSession: () => unsupported(),165 startSession: () => unsupported(),
166 sendTurn: () => unsupported(),166 sendTurn: () => unsupported(),
167 compactThread: () => unsupported(),
168 interruptTurn: () => unsupported(),167 interruptTurn: () => unsupported(),
169 respondToRequest: () => unsupported(),168 respondToRequest: () => unsupported(),
170 respondToUserInput: () => unsupported(),169 respondToUserInput: () => unsupported(),
apps/server/src/provider/Services/ProviderAdapter.ts-5
...@@ -68,11 +68,6 @@ export interface ProviderAdapterShape<TError> {...@@ -68,11 +68,6 @@ export interface ProviderAdapterShape<TError> {
68 input: ProviderSendTurnInput,68 input: ProviderSendTurnInput,
69 ) => Effect.Effect<ProviderTurnStartResult, TError>;69 ) => Effect.Effect<ProviderTurnStartResult, TError>;
7070
71 readonly compactThread?: (
72 threadId: ThreadId,
73 modelSelection?: ProviderSendTurnInput["modelSelection"],
74 ) => Effect.Effect<void, TError>;
75
76 /**71 /**
77 * Interrupt an active turn.72 * Interrupt an active turn.
78 */73 */
apps/server/src/provider/Services/ProviderService.ts-5
...@@ -53,11 +53,6 @@ export interface ProviderServiceShape {...@@ -53,11 +53,6 @@ export interface ProviderServiceShape {
53 input: ProviderSendTurnInput,53 input: ProviderSendTurnInput,
54 ) => Effect.Effect<ProviderTurnStartResult, ProviderServiceError>;54 ) => Effect.Effect<ProviderTurnStartResult, ProviderServiceError>;
5555
56 readonly compactThread: (
57 threadId: ThreadId,
58 modelSelection?: ProviderSendTurnInput["modelSelection"],
59 ) => Effect.Effect<void, ProviderServiceError>;
60
61 /**56 /**
62 * Interrupt a running provider turn.57 * Interrupt a running provider turn.
63 */58 */
apps/server/src/provider/providerSnapshot.ts-5
...@@ -22,11 +22,6 @@ export const DEFAULT_TIMEOUT_MS = 4_000;...@@ -22,11 +22,6 @@ export const DEFAULT_TIMEOUT_MS = 4_000;
22// Auth status checks involve disk/network lookups and can be slow on first run (especially Windows)22// Auth status checks involve disk/network lookups and can be slow on first run (especially Windows)
23export const AUTH_PROBE_TIMEOUT_MS = 10_000;23export const AUTH_PROBE_TIMEOUT_MS = 10_000;
2424
25export const COMPACT_SLASH_COMMAND = {
26 name: "compact",
27 description: "Summarize the conversation and reduce context usage",
28} satisfies ServerProviderSlashCommand;
29
30export interface CommandResult {25export interface CommandResult {
31 readonly stdout: string;26 readonly stdout: string;
32 readonly stderr: string;27 readonly stderr: string;
apps/server/src/serverRuntimeStartup.reconcile.test.ts-1
...@@ -51,7 +51,6 @@ const makeProviderService = (liveThreadIds: ReadonlyArray<ThreadId> = []) =>...@@ -51,7 +51,6 @@ const makeProviderService = (liveThreadIds: ReadonlyArray<ThreadId> = []) =>
51 ({51 ({
52 startSession: () => Effect.die("unused"),52 startSession: () => Effect.die("unused"),
53 sendTurn: () => Effect.die("unused"),53 sendTurn: () => Effect.die("unused"),
54 compactThread: () => Effect.die("unused"),
55 interruptTurn: () => Effect.die("unused"),54 interruptTurn: () => Effect.die("unused"),
56 respondToRequest: () => Effect.die("unused"),55 respondToRequest: () => Effect.die("unused"),
57 respondToUserInput: () => Effect.die("unused"),56 respondToUserInput: () => Effect.die("unused"),
apps/web/src/components/ChatView.tsx+19-21
...@@ -328,7 +328,7 @@ import {...@@ -328,7 +328,7 @@ import {
328import type { ComposerBannerStackItem } from "./chat/ComposerBannerStack";328import type { ComposerBannerStackItem } from "./chat/ComposerBannerStack";
329import { ComposerSurface } from "./chat/ComposerSurface";329import { ComposerSurface } from "./chat/ComposerSurface";
330import {330import {
331 hasAvailableCompactionProvider,331 hasAvailableClaudeCompactionProvider,
332 hasDismissedResumeCompaction,332 hasDismissedResumeCompaction,
333 shouldOfferResumeCompaction,333 shouldOfferResumeCompaction,
334} from "./chat/ContextWindowMeter.logic";334} from "./chat/ContextWindowMeter.logic";
...@@ -2947,23 +2947,13 @@ function ChatViewContent(props: ChatViewProps) {...@@ -2947,23 +2947,13 @@ function ChatViewContent(props: ChatViewProps) {
2947 activeThread?.modelSelection.instanceId ??2947 activeThread?.modelSelection.instanceId ??
2948 activeProject?.defaultModelSelection?.instanceId ??2948 activeProject?.defaultModelSelection?.instanceId ??
2949 null;2949 null;
2950 const activeProviderStatus = useMemo(() => {2950 const compactionProviderAvailable = useMemo(
2951 if (activeProviderInstanceId) {
2952 return (
2953 providerStatuses.find((status) => status.instanceId === activeProviderInstanceId) ?? null
2954 );
2955 }
2956 const defaultInstanceId = defaultInstanceIdForDriver(selectedProvider);
2957 return providerStatuses.find((status) => status.instanceId === defaultInstanceId) ?? null;
2958 }, [activeProviderInstanceId, providerStatuses, selectedProvider]);
2959 const manualCompactionProviderAvailable = useMemo(
2960 () =>2951 () =>
2961 hasAvailableCompactionProvider({2952 hasAvailableClaudeCompactionProvider({
2962 providers: applyProviderInstanceSettings(2953 providers: applyProviderInstanceSettings(
2963 deriveProviderInstanceEntries(providerStatuses),2954 deriveProviderInstanceEntries(providerStatuses),
2964 settings,2955 settings,
2965 ),2956 ),
2966 driverKind: selectedProvider,
2967 instanceId: activeProviderInstanceId,2957 instanceId: activeProviderInstanceId,
2968 lockedInstanceId: lockedProvider2958 lockedInstanceId: lockedProvider
2969 ? (activeThread?.session?.providerInstanceId ??2959 ? (activeThread?.session?.providerInstanceId ??
...@@ -2977,10 +2967,18 @@ function ChatViewContent(props: ChatViewProps) {...@@ -2977,10 +2967,18 @@ function ChatViewContent(props: ChatViewProps) {
2977 activeThread?.session?.providerInstanceId,2967 activeThread?.session?.providerInstanceId,
2978 lockedProvider,2968 lockedProvider,
2979 providerStatuses,2969 providerStatuses,
2980 selectedProvider,
2981 settings,2970 settings,
2982 ],2971 ],
2983 );2972 );
2973 const activeProviderStatus = useMemo(() => {
2974 if (activeProviderInstanceId) {
2975 return (
2976 providerStatuses.find((status) => status.instanceId === activeProviderInstanceId) ?? null
2977 );
2978 }
2979 const defaultInstanceId = defaultInstanceIdForDriver(selectedProvider);
2980 return providerStatuses.find((status) => status.instanceId === defaultInstanceId) ?? null;
2981 }, [activeProviderInstanceId, providerStatuses, selectedProvider]);
2984 const [resumeCompactionPermanentlyDismissed, setResumeCompactionPermanentlyDismissed] =2982 const [resumeCompactionPermanentlyDismissed, setResumeCompactionPermanentlyDismissed] =
2985 useLocalStorage(2983 useLocalStorage(
2986 `t3code:resume-compaction-dismissed:${environmentId}:${activeProviderInstanceId ?? "claudeAgent"}`,2984 `t3code:resume-compaction-dismissed:${environmentId}:${activeProviderInstanceId ?? "claudeAgent"}`,
...@@ -5246,11 +5244,12 @@ function ChatViewContent(props: ChatViewProps) {...@@ -5246,11 +5244,12 @@ function ChatViewContent(props: ChatViewProps) {
5246 activeThread && activeContextWindow5244 activeThread && activeContextWindow
5247 ? `${activeThread.id}:${activeContextWindow.updatedAt}`5245 ? `${activeThread.id}:${activeContextWindow.updatedAt}`
5248 : null;5246 : null;
5249 const compactThreadUnavailable =5247 const compactDisabled =
5250 !activeThread ||5248 !activeThread ||
5251 !activeProject ||5249 !activeProject ||
5252 !isServerThread ||5250 !isServerThread ||
5253 !manualCompactionProviderAvailable ||5251 selectedProvider !== "claudeAgent" ||
5252 !compactionProviderAvailable ||
5254 isWorking ||5253 isWorking ||
5255 threadDetailLoading ||5254 threadDetailLoading ||
5256 isPreparingWorktree ||5255 isPreparingWorktree ||
...@@ -5258,15 +5257,15 @@ function ChatViewContent(props: ChatViewProps) {...@@ -5258,15 +5257,15 @@ function ChatViewContent(props: ChatViewProps) {
5258 feedbackUploading ||5257 feedbackUploading ||
5259 pendingApprovals.length > 0 ||5258 pendingApprovals.length > 0 ||
5260 pendingUserInputs.length > 0 ||5259 pendingUserInputs.length > 0 ||
5261 showPlanFollowUpPrompt;5260 showPlanFollowUpPrompt ||
5262 const compactDisabled = compactThreadUnavailable || composerHasUnsentContent;5261 composerHasUnsentContent;
5263 const compactDisabledReason = compactDisabled5262 const compactDisabledReason = compactDisabled
5264 ? composerHasUnsentContent5263 ? composerHasUnsentContent
5265 ? "Send or clear your draft before compacting"5264 ? "Send or clear your draft before compacting"
5266 : !activeProject5265 : !activeProject
5267 ? "Choose a project before compacting"5266 ? "Choose a project before compacting"
5268 : !manualCompactionProviderAvailable5267 : !compactionProviderAvailable
5269 ? "Compaction is unavailable for this provider"5268 ? "Enable a Claude provider before compacting"
5270 : "Compacting is unavailable right now"5269 : "Compacting is unavailable right now"
5271 : null;5270 : null;
5272 const resumeCompactionBannerItem = useMemo<ComposerBannerStackItem | null>(() => {5271 const resumeCompactionBannerItem = useMemo<ComposerBannerStackItem | null>(() => {
...@@ -7650,7 +7649,6 @@ function ChatViewContent(props: ChatViewProps) {...@@ -7650,7 +7649,6 @@ function ChatViewContent(props: ChatViewProps) {
7650 }7649 }
7651 activeThreadModelSelection={activeThread?.modelSelection}7650 activeThreadModelSelection={activeThread?.modelSelection}
7652 activeContextWindow={activeContextWindow}7651 activeContextWindow={activeContextWindow}
7653 compactThreadUnavailable={compactThreadUnavailable}
7654 compactDisabled={compactDisabled}7652 compactDisabled={compactDisabled}
7655 compactDisabledReason={compactDisabledReason}7653 compactDisabledReason={compactDisabledReason}
7656 resolvedTheme={resolvedTheme}7654 resolvedTheme={resolvedTheme}
apps/web/src/components/chat/ChatComposer.tsx+8-23
...@@ -163,10 +163,7 @@ import {...@@ -163,10 +163,7 @@ import {
163 renderProviderTraitsPicker,163 renderProviderTraitsPicker,
164} from "./composerProviderState";164} from "./composerProviderState";
165import { ContextWindowMeter } from "./ContextWindowMeter";165import { ContextWindowMeter } from "./ContextWindowMeter";
166import {166import { resolveContextWindowModelDisplayName } from "./ContextWindowMeter.logic";
167 providerSupportsManualCompaction,
168 resolveContextWindowModelDisplayName,
169} from "./ContextWindowMeter.logic";
170import {167import {
171 attachVideoThumbnail,168 attachVideoThumbnail,
172 buildExpandedImagePreview,169 buildExpandedImagePreview,
...@@ -694,7 +691,6 @@ export interface ChatComposerProps {...@@ -694,7 +691,6 @@ export interface ChatComposerProps {
694691
695 // Context window692 // Context window
696 activeContextWindow: ContextWindowSnapshot | null;693 activeContextWindow: ContextWindowSnapshot | null;
697 compactThreadUnavailable: boolean;
698 compactDisabled: boolean;694 compactDisabled: boolean;
699 compactDisabledReason: string | null;695 compactDisabledReason: string | null;
700696
...@@ -791,7 +787,6 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)...@@ -791,7 +787,6 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
791 activeProjectDefaultModelSelection,787 activeProjectDefaultModelSelection,
792 activeThreadModelSelection,788 activeThreadModelSelection,
793 activeContextWindow,789 activeContextWindow,
794 compactThreadUnavailable,
795 compactDisabled,790 compactDisabled,
796 compactDisabledReason,791 compactDisabledReason,
797 resolvedTheme,792 resolvedTheme,
...@@ -1125,7 +1120,6 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)...@@ -1125,7 +1120,6 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
1125 () => selectedProviderEntry?.snapshot ?? null,1120 () => selectedProviderEntry?.snapshot ?? null,
1126 [selectedProviderEntry],1121 [selectedProviderEntry],
1127 );1122 );
1128 const compactCommandAvailable = providerSupportsManualCompaction(selectedProviderEntry);
1129 const selectedProviderSkills = selectedProviderStatus1123 const selectedProviderSkills = selectedProviderStatus
1130 ? resolveProviderSkillsForCwd(selectedProviderStatus, gitCwd)1124 ? resolveProviderSkillsForCwd(selectedProviderStatus, gitCwd)
1131 : [];1125 : [];
...@@ -1354,6 +1348,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)...@@ -1354,6 +1348,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
1354 prompt,1348 prompt,
1355 ],1349 ],
1356 );1350 );
1351
1357 // ------------------------------------------------------------------1352 // ------------------------------------------------------------------
1358 // Derived: composer trigger / menu1353 // Derived: composer trigger / menu
1359 // ------------------------------------------------------------------1354 // ------------------------------------------------------------------
...@@ -1365,16 +1360,6 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)...@@ -1365,16 +1360,6 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
1365 cwd: isPathTrigger ? gitCwd : null,1360 cwd: isPathTrigger ? gitCwd : null,
1366 query: isPathTrigger ? pathTriggerQuery : null,1361 query: isPathTrigger ? pathTriggerQuery : null,
1367 });1362 });
1368 const compactSlashCommandAvailable =
1369 composerTrigger?.kind === "slash-command" &&
1370 !compactThreadUnavailable &&
1371 prompt.slice(composerTrigger.rangeEnd).trim() === "" &&
1372 composerImages.length + composerFiles.length === 0 &&
1373 composerDraft.persistedAttachments.length === 0 &&
1374 composerTerminalContexts.length === 0 &&
1375 composerElementContexts.length === 0 &&
1376 composerPreviewAnnotations.length === 0 &&
1377 composerReviewComments.length === 0;
13781363
1379 const composerMenuItems = useMemo<ComposerCommandItem[]>(() => {1364 const composerMenuItems = useMemo<ComposerCommandItem[]>(() => {
1380 if (!composerTrigger) return [];1365 if (!composerTrigger) return [];
...@@ -1443,11 +1428,8 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)...@@ -1443,11 +1428,8 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
1443 skill.description ??1428 skill.description ??
1444 (skill.scope ? `${skill.scope} skill` : ""),1429 (skill.scope ? `${skill.scope} skill` : ""),
1445 }));1430 }));
1446 const visibleProviderSlashCommandItems = providerSlashCommandItems.filter(
1447 (item) => item.command.name !== "compact" || compactSlashCommandAvailable,
1448 );
1449 const slashCommandItems = slashCommandItemsForPromptPosition(1431 const slashCommandItems = slashCommandItemsForPromptPosition(
1450 [...builtInSlashCommandItems, ...visibleProviderSlashCommandItems, ...skillItems],1432 [...builtInSlashCommandItems, ...providerSlashCommandItems, ...skillItems],
1451 composerTrigger.rangeStart === 0,1433 composerTrigger.rangeStart === 0,
1452 );1434 );
1453 return searchSlashCommandItems(slashCommandItems, query);1435 return searchSlashCommandItems(slashCommandItems, query);
...@@ -1467,7 +1449,6 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)...@@ -1467,7 +1449,6 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
1467 }1449 }
1468 return [];1450 return [];
1469 }, [1451 }, [
1470 compactSlashCommandAvailable,
1471 composerTrigger,1452 composerTrigger,
1472 planModeUiEnabled,1453 planModeUiEnabled,
1473 selectedProvider,1454 selectedProvider,
...@@ -2317,6 +2298,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)...@@ -2317,6 +2298,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
2317 if (2298 if (
2318 compactDisabled ||2299 compactDisabled ||
2319 noProviderAvailable ||2300 noProviderAvailable ||
2301 composerSendState.hasSendableContent ||
2320 activePendingApproval !== null ||2302 activePendingApproval !== null ||
2321 pendingUserInputs.length > 0 ||2303 pendingUserInputs.length > 0 ||
2322 phase === "running" ||2304 phase === "running" ||
...@@ -2354,6 +2336,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)...@@ -2354,6 +2336,7 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
2354 activeThreadId,2336 activeThreadId,
2355 compactDisabled,2337 compactDisabled,
2356 composerDraftTarget,2338 composerDraftTarget,
2339 composerSendState.hasSendableContent,
2357 isConnecting,2340 isConnecting,
2358 isSendBusy,2341 isSendBusy,
2359 noProviderAvailable,2342 noProviderAvailable,
...@@ -4419,7 +4402,9 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)...@@ -4419,7 +4402,9 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
4419 compactDisabled || noProviderAvailable || isSendBusy || isConnecting4402 compactDisabled || noProviderAvailable || isSendBusy || isConnecting
4420 }4403 }
4421 compactDisabledReason={resolvedCompactDisabledReason}4404 compactDisabledReason={resolvedCompactDisabledReason}
4422 {...(compactCommandAvailable ? { onCompactContext: compactThreadContext } : {})}4405 {...(selectedProvider === "claudeAgent"
4406 ? { onCompactContext: compactThreadContext }
4407 : {})}
4423 />4408 />
4424 </div>4409 </div>
4425 </div>4410 </div>
apps/web/src/components/chat/ContextWindowMeter.logic.test.ts+5-7
...@@ -3,7 +3,7 @@ import { describe, expect, it } from "vite-plus/test";...@@ -3,7 +3,7 @@ import { describe, expect, it } from "vite-plus/test";
3import { deriveProviderInstanceEntries } from "../../providerInstances";3import { deriveProviderInstanceEntries } from "../../providerInstances";
4import {4import {
5 formatContextWindowCompactionMessage,5 formatContextWindowCompactionMessage,
6 hasAvailableCompactionProvider,6 hasAvailableClaudeCompactionProvider,
7 hasDismissedResumeCompaction,7 hasDismissedResumeCompaction,
8 resolveContextWindowModelDisplayName,8 resolveContextWindowModelDisplayName,
9 shouldOfferResumeCompaction,9 shouldOfferResumeCompaction,
...@@ -25,12 +25,12 @@ function claudeProvider(input: {...@@ -25,12 +25,12 @@ function claudeProvider(input: {
25 auth: { status: "authenticated" },25 auth: { status: "authenticated" },
26 checkedAt: "2026-08-24T12:00:00.000Z",26 checkedAt: "2026-08-24T12:00:00.000Z",
27 models: [],27 models: [],
28 slashCommands: [{ name: "compact", description: "" }],28 slashCommands: [],
29 skills: [],29 skills: [],
30 };30 };
31}31}
3232
33describe("hasAvailableCompactionProvider", () => {33describe("hasAvailableClaudeCompactionProvider", () => {
34 const originalInstanceId = ProviderInstanceId.make("claude_original");34 const originalInstanceId = ProviderInstanceId.make("claude_original");
3535
36 it("rejects a fallback in a different locked continuation group", () => {36 it("rejects a fallback in a different locked continuation group", () => {
...@@ -47,9 +47,8 @@ describe("hasAvailableCompactionProvider", () => {...@@ -47,9 +47,8 @@ describe("hasAvailableCompactionProvider", () => {
47 ]);47 ]);
4848
49 expect(49 expect(
50 hasAvailableCompactionProvider({50 hasAvailableClaudeCompactionProvider({
51 providers,51 providers,
52 driverKind: ProviderDriverKind.make("claudeAgent"),
53 instanceId: originalInstanceId,52 instanceId: originalInstanceId,
54 lockedInstanceId: originalInstanceId,53 lockedInstanceId: originalInstanceId,
55 }),54 }),
...@@ -70,9 +69,8 @@ describe("hasAvailableCompactionProvider", () => {...@@ -70,9 +69,8 @@ describe("hasAvailableCompactionProvider", () => {
70 ]);69 ]);
7170
72 expect(71 expect(
73 hasAvailableCompactionProvider({72 hasAvailableClaudeCompactionProvider({
74 providers,73 providers,
75 driverKind: ProviderDriverKind.make("claudeAgent"),
76 instanceId: originalInstanceId,74 instanceId: originalInstanceId,
77 lockedInstanceId: originalInstanceId,75 lockedInstanceId: originalInstanceId,
78 }),76 }),
apps/web/src/components/chat/ContextWindowMeter.logic.ts+10-16
...@@ -1,4 +1,4 @@...@@ -1,4 +1,4 @@
1import type { ModelSelection, ProviderDriverKind, ProviderInstanceId } from "@t3tools/contracts";1import type { ModelSelection, ProviderInstanceId } from "@t3tools/contracts";
2import {2import {
3 CLAUDE_RESUME_COMPACTION_NEVER_ANSWER,3 CLAUDE_RESUME_COMPACTION_NEVER_ANSWER,
4 isClaudeResumeCompactionQuestion,4 isClaudeResumeCompactionQuestion,
...@@ -12,33 +12,27 @@ import { getTriggerDisplayModelName, type ModelEsque } from "./providerIconUtils...@@ -12,33 +12,27 @@ import { getTriggerDisplayModelName, type ModelEsque } from "./providerIconUtils
12export const CLAUDE_RESUME_COMPACTION_MINUTES = 70;12export const CLAUDE_RESUME_COMPACTION_MINUTES = 70;
13export const CLAUDE_RESUME_COMPACTION_TOKENS = 100_000;13export const CLAUDE_RESUME_COMPACTION_TOKENS = 100_000;
1414
15export function providerSupportsManualCompaction(15export function hasAvailableClaudeCompactionProvider(input: {
16 provider: ProviderInstanceEntry | null | undefined,
17): boolean {
18 return provider?.snapshot.slashCommands.some((command) => command.name === "compact") ?? false;
19}
20
21export function hasAvailableCompactionProvider(input: {
22 readonly providers: ReadonlyArray<ProviderInstanceEntry>;16 readonly providers: ReadonlyArray<ProviderInstanceEntry>;
23 readonly driverKind: ProviderDriverKind;
24 readonly instanceId: ProviderInstanceId | null;17 readonly instanceId: ProviderInstanceId | null;
25 readonly lockedInstanceId: ProviderInstanceId | null;18 readonly lockedInstanceId: ProviderInstanceId | null;
26}): boolean {19}): boolean {
27 const driverProviders = input.providers.filter(20 const claudeProviders = input.providers.filter(
28 (provider) => provider.driverKind === input.driverKind,21 (provider) => provider.driverKind === "claudeAgent",
29 );22 );
30 const lockedContinuationGroupKey = input.lockedInstanceId23 const lockedContinuationGroupKey = input.lockedInstanceId
31 ? driverProviders.find((provider) => provider.instanceId === input.lockedInstanceId)24 ? claudeProviders.find((provider) => provider.instanceId === input.lockedInstanceId)
32 ?.continuationGroupKey25 ?.continuationGroupKey
33 : undefined;26 : undefined;
34 const compatibleProviders = lockedContinuationGroupKey27 const compatibleProviders = lockedContinuationGroupKey
35 ? driverProviders.filter(28 ? claudeProviders.filter(
36 (provider) => provider.continuationGroupKey === lockedContinuationGroupKey,29 (provider) => provider.continuationGroupKey === lockedContinuationGroupKey,
37 )30 )
38 : driverProviders;31 : claudeProviders;
3932
40 return providerSupportsManualCompaction(33 return (
41 resolveSelectableProviderInstanceEntry(compatibleProviders, input.instanceId ?? undefined),34 resolveSelectableProviderInstanceEntry(compatibleProviders, input.instanceId ?? undefined) !==
35 undefined
42 );36 );
43}37}
4438
apps/web/src/components/chat/MessagesTimeline.test.tsx+2-2
...@@ -919,7 +919,7 @@ describe("MessagesTimeline", () => {...@@ -919,7 +919,7 @@ describe("MessagesTimeline", () => {
919 entry: {919 entry: {
920 id: "work-1",920 id: "work-1",
921 createdAt: "2026-03-17T19:12:28.000Z",921 createdAt: "2026-03-17T19:12:28.000Z",
922 label: "Compacted context 899K → 19K tokens",922 label: "Context compacted",
923 tone: "info",923 tone: "info",
924 },924 },
925 },925 },
...@@ -927,7 +927,7 @@ describe("MessagesTimeline", () => {...@@ -927,7 +927,7 @@ describe("MessagesTimeline", () => {
927 />,927 />,
928 );928 );
929929
930 expect(markup).toContain("Compacted context 899K → 19K tokens");930 expect(markup).toContain("Context compacted");
931 });931 });
932932
933 it("summarizes changed files in one line", () => {933 it("summarizes changed files in one line", () => {
docs/user/composer.md-2
...@@ -196,8 +196,6 @@ such as System, Personal, Project, or App....@@ -196,8 +196,6 @@ such as System, Personal, Project, or App.
196On mobile, these menus are available on the **New task** screen before you start a thread. They196On mobile, these menus are available on the **New task** screen before you start a thread. They
197use the skills and commands from the selected environment and provider.197use the skills and commands from the selected environment and provider.
198198
199In an existing thread, send `/compact` to reduce context usage. Web and desktop also offer this action from the context meter, and the work log records token counts when the provider reports them.
200
201By default, the `/` menu includes skills. To keep this menu command-only, turn off **Show skills in199By default, the `/` menu includes skills. To keep this menu command-only, turn off **Show skills in
202slash menu** in **Settings → General**. Skill results use the `/skill:Skill Name` label and add the200slash menu** in **Settings → General**. Skill results use the `/skill:Skill Name` label and add the
203same `$name` skill token to your message. The original skill name remains searchable. If the provider201same `$name` skill token to your message. The original skill name remains searchable. If the provider
packages/contracts/src/providerRuntime.ts-2
...@@ -298,8 +298,6 @@ export type ThreadStartedPayload = typeof ThreadStartedPayload.Type;...@@ -298,8 +298,6 @@ export type ThreadStartedPayload = typeof ThreadStartedPayload.Type;
298298
299const ThreadStateChangedPayload = Schema.Struct({299const ThreadStateChangedPayload = Schema.Struct({
300 state: RuntimeThreadState,300 state: RuntimeThreadState,
301 beforeTokens: Schema.optional(NonNegativeInt),
302 afterTokens: Schema.optional(NonNegativeInt),
303 detail: Schema.optional(Schema.Unknown),301 detail: Schema.optional(Schema.Unknown),
304});302});
305export type ThreadStateChangedPayload = typeof ThreadStateChangedPayload.Type;303export type ThreadStateChangedPayload = typeof ThreadStateChangedPayload.Type;