@love-moon/ai-sdk 0.13.0 → 0.13.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,40 @@
1
1
  # @love-moon/ai-sdk
2
2
 
3
+ ## 0.13.2
4
+
5
+ ### Patch Changes
6
+
7
+ - 8c2833d: Support `/compact [focus instructions]` in task chats. Fire detects the command
8
+ per message (like `/goal`), runs the backend's native context compaction
9
+ instead of sending the text to the model, and posts one confirmation with the
10
+ token savings. Sessions advertise `capabilities.compact` and implement the new
11
+ optional `runCompact()`: claude (native `/compact`), codex app-server
12
+ (`thread/compact/start`), copilot (`session.history.compact`), kimi wire and
13
+ legacy print (built-in `/compact`), opencode (`session.summarize`) and dsh
14
+ (summary turn, then a fresh session seeded with the summary). Backends without
15
+ the capability (chat-web, codex exec, Kimi Code prompt mode) reply with a
16
+ not-supported notice. Opencode compaction summaries no longer surface as chat
17
+ replies, and dsh clears its "compacting context" status when automatic
18
+ compaction ends.
19
+ - 8e3a4c3: Report how many model tokens each turn consumed. After every turn (or `/goal`)
20
+ `conductor fire` sends the turn's token count (fresh input + cache reads/writes +
21
+ output) over the new `task_turn_usage` websocket event, and the server adds it
22
+ to the task's running total shown in the task detail card. A persistent task's
23
+ total restarts with each round.
24
+
25
+ Failed or interrupted turns and `/compact` are counted too; when a turn's usage
26
+ is unknown the fire reports `null`, which clears the task's last-turn count.
27
+
28
+ - `@love-moon/conductor-sdk`: new `ConductorClient.sendTurnUsage(taskId, { tokens })`.
29
+ - `@love-moon/ai-sdk`: the Codex app-server provider's turn/goal `usage` now
30
+ carries `turnTotalTokens`, the turn's share of Codex's thread-cumulative total.
31
+ Claude and Codex turn errors carry the tokens spent before the failure as
32
+ `error.usage` (Claude sums streamed usage when an interrupted query ends
33
+ without a result).
34
+ - Claude and Codex backends are counted; other backends report nothing yet.
35
+
36
+ ## 0.13.1
37
+
3
38
  ## 0.13.0
4
39
 
5
40
  ## 0.12.0
package/dist/client.d.ts CHANGED
@@ -18,6 +18,7 @@ export class RemoteAiSession extends EventEmitter<[never]> {
18
18
  workerReady: boolean;
19
19
  capabilities: {
20
20
  goal?: boolean | undefined;
21
+ compact?: boolean | undefined;
21
22
  };
22
23
  };
23
24
  sessionMessageHandler: any;
@@ -51,11 +52,17 @@ export class RemoteAiSession extends EventEmitter<[never]> {
51
52
  workerReady: boolean;
52
53
  capabilities: {
53
54
  goal?: boolean | undefined;
55
+ compact?: boolean | undefined;
54
56
  };
55
57
  };
56
58
  usesSessionFileReplyStream(): boolean;
57
59
  getSessionInfo(): any;
58
60
  getCurrentTurnStatus(): any;
61
+ /**
62
+ * Ask the worker for the live turn status (including `active_tool`). Uses
63
+ * the control lane so it answers while `runTurn` holds the request queue.
64
+ */
65
+ fetchCurrentTurnStatus(): Promise<any>;
59
66
  setSessionMessageHandler(handler: any): void;
60
67
  setWorkingStatusHandler(handler: any): void;
61
68
  setSessionReplyTarget(replyTo: any): void;
@@ -64,6 +71,7 @@ export class RemoteAiSession extends EventEmitter<[never]> {
64
71
  interruptCurrentTurn(): Promise<any>;
65
72
  runTurn(promptText: any, options?: {}): Promise<any>;
66
73
  runGoal(goal: any, options?: {}): Promise<any>;
74
+ runCompact(request?: {}, options?: {}): Promise<any>;
67
75
  getGoal(): Promise<any>;
68
76
  clearGoal(): Promise<any>;
69
77
  close(): Promise<void>;
@@ -103,6 +111,7 @@ declare class LocalAiSessionProxy extends EventEmitter<[never]> {
103
111
  workerReady: boolean;
104
112
  capabilities: {
105
113
  goal?: boolean | undefined;
114
+ compact?: boolean | undefined;
106
115
  };
107
116
  };
108
117
  readyPromise: Promise<any>;
@@ -121,6 +130,7 @@ declare class LocalAiSessionProxy extends EventEmitter<[never]> {
121
130
  interruptCurrentTurn(): Promise<any>;
122
131
  runTurn(promptText: any, options?: {}): Promise<any>;
123
132
  runGoal(goal: any, options?: {}): Promise<any>;
133
+ runCompact(request?: {}, options?: {}): Promise<any>;
124
134
  getGoal(): Promise<any>;
125
135
  clearGoal(): Promise<any>;
126
136
  close(): Promise<void>;
package/dist/client.js CHANGED
@@ -9,6 +9,11 @@ function createUnsupportedGoalError(name) {
9
9
  err.reason = "unsupported_goal";
10
10
  return err;
11
11
  }
12
+ function createUnsupportedCompactError(name) {
13
+ const err = new Error(`runCompact() is not supported by backend session (${name || "unknown"}).`);
14
+ err.reason = "unsupported_compact";
15
+ return err;
16
+ }
12
17
  const WORKER_PATH = fileURLToPath(new URL("./worker.js", import.meta.url));
13
18
  function sanitizeOptionsForWorker(options = {}) {
14
19
  const sanitized = {};
@@ -155,6 +160,17 @@ export class RemoteAiSession extends EventEmitter {
155
160
  getCurrentTurnStatus() {
156
161
  return this.currentTurnStatus ? { ...this.currentTurnStatus } : null;
157
162
  }
163
+ /**
164
+ * Ask the worker for the live turn status (including `active_tool`). Uses
165
+ * the control lane so it answers while `runTurn` holds the request queue.
166
+ */
167
+ async fetchCurrentTurnStatus() {
168
+ const status = await this.callWorker("getCurrentTurnStatus", [], { messageType: "control" });
169
+ if (status && typeof status === "object") {
170
+ this.currentTurnStatus = { ...status };
171
+ }
172
+ return this.getCurrentTurnStatus();
173
+ }
158
174
  setSessionMessageHandler(handler) {
159
175
  this.sessionMessageHandler = typeof handler === "function" ? handler : null;
160
176
  }
@@ -172,7 +188,9 @@ export class RemoteAiSession extends EventEmitter {
172
188
  return this.callWorker("ensureSessionInfo", []);
173
189
  }
174
190
  async getSessionUsageSummary() {
175
- return this.callWorker("getSessionUsageSummary", []);
191
+ // Control lane: fire awaits this before every runtime-status report, so
192
+ // queueing it behind a running `runTurn` would stall mid-turn statuses.
193
+ return this.callWorker("getSessionUsageSummary", [], { messageType: "control" });
176
194
  }
177
195
  async interruptCurrentTurn() {
178
196
  return this.callWorker("interruptCurrentTurn", [], {
@@ -204,6 +222,16 @@ export class RemoteAiSession extends EventEmitter {
204
222
  progressHandler: typeof onProgress === "function" ? onProgress : null,
205
223
  });
206
224
  }
225
+ async runCompact(request = {}, options = {}) {
226
+ await this.readyPromise;
227
+ if (!this.snapshot?.capabilities || this.snapshot.capabilities.compact !== true) {
228
+ throw createUnsupportedCompactError(this.snapshot?.provider || this.snapshot?.backend);
229
+ }
230
+ const { onProgress, ...restOptions } = options || {};
231
+ return this.callWorker("runCompact", [request, restOptions], {
232
+ progressHandler: typeof onProgress === "function" ? onProgress : null,
233
+ });
234
+ }
207
235
  async getGoal() {
208
236
  // Propagate worker-creation errors instead of masking them as "no goal".
209
237
  // See N3 in the review.
@@ -662,6 +690,15 @@ class LocalAiSessionProxy extends EventEmitter {
662
690
  }
663
691
  return await session.runGoal(goal, options);
664
692
  }
693
+ async runCompact(request = {}, options = {}) {
694
+ const session = await this.readyPromise;
695
+ // Capabilities resolved at init (snapshot first) are authoritative; some
696
+ // providers only advertise them via getSnapshot().
697
+ if (this.snapshot?.capabilities?.compact !== true || typeof session.runCompact !== "function") {
698
+ throw createUnsupportedCompactError(session?.constructor?.name);
699
+ }
700
+ return await session.runCompact(request, options);
701
+ }
665
702
  async getGoal() {
666
703
  const session = await this.readyPromise;
667
704
  const capabilities = resolveSessionCapabilities(session);
@@ -9,6 +9,7 @@ export function resolveClaudeCommandForRoot(commandLine: any, env?: NodeJS.Proce
9
9
  export class ClaudeAgentSdkSession extends EventEmitter<[never]> {
10
10
  static capabilities: Readonly<{
11
11
  goal: true;
12
+ compact: true;
12
13
  media: Readonly<{
13
14
  image: "native";
14
15
  }>;
@@ -16,6 +17,7 @@ export class ClaudeAgentSdkSession extends EventEmitter<[never]> {
16
17
  constructor(backend: any, options?: {});
17
18
  getCapabilities(): {
18
19
  goal: true;
20
+ compact: true;
19
21
  media: Readonly<{
20
22
  image: "native";
21
23
  }>;
@@ -42,12 +44,16 @@ export class ClaudeAgentSdkSession extends EventEmitter<[never]> {
42
44
  manualResumeReady: boolean;
43
45
  currentTurn: {
44
46
  abortController: AbortController;
47
+ suppressReply: boolean;
48
+ compactMetadata: null;
49
+ compactError: string;
45
50
  emittedAssistantMessage: boolean;
46
51
  fullText: string;
47
52
  items: never[];
48
53
  query: null;
49
54
  resultMessage: null;
50
55
  terminalWorkingStatusEmitted: boolean;
56
+ usageByMessageId: Map<any, any>;
51
57
  } | null;
52
58
  lastResult: any;
53
59
  rateLimitInfo: any;
@@ -88,6 +94,7 @@ export class ClaudeAgentSdkSession extends EventEmitter<[never]> {
88
94
  currentTurnStatus: any;
89
95
  capabilities: {
90
96
  goal: true;
97
+ compact: true;
91
98
  media: Readonly<{
92
99
  image: "native";
93
100
  }>;
@@ -154,10 +161,11 @@ export class ClaudeAgentSdkSession extends EventEmitter<[never]> {
154
161
  onProgress: any;
155
162
  }): Promise<void>;
156
163
  interruptCurrentTurn(): Promise<boolean>;
157
- runTurn(promptText: any, { useInitialImages, media: mediaInput, contextFiles, onProgress, jsonSchema }?: {
164
+ runTurn(promptText: any, { useInitialImages, media: mediaInput, contextFiles, onProgress, jsonSchema, suppressReply }?: {
158
165
  useInitialImages?: boolean | undefined;
159
166
  onProgress?: null | undefined;
160
167
  jsonSchema?: null | undefined;
168
+ suppressReply?: boolean | undefined;
161
169
  }): Promise<{
162
170
  text: string;
163
171
  usage: null;
@@ -165,6 +173,8 @@ export class ClaudeAgentSdkSession extends EventEmitter<[never]> {
165
173
  events: never[];
166
174
  provider?: undefined;
167
175
  metadata?: undefined;
176
+ compactMetadata?: undefined;
177
+ compactError?: undefined;
168
178
  } | {
169
179
  text: any;
170
180
  usage: any;
@@ -178,6 +188,8 @@ export class ClaudeAgentSdkSession extends EventEmitter<[never]> {
178
188
  modelUsage: any;
179
189
  structuredOutput: any;
180
190
  };
191
+ compactMetadata: undefined;
192
+ compactError: string | undefined;
181
193
  }>;
182
194
  /**
183
195
  * Trigger Claude's native `/goal` slash command. Implemented by sending the
@@ -204,6 +216,18 @@ export class ClaudeAgentSdkSession extends EventEmitter<[never]> {
204
216
  * @returns {Promise<import("../shared.js").GoalResult>}
205
217
  */
206
218
  runGoal(goal: import("../shared.js").GoalRequest, options?: object): Promise<import("../shared.js").GoalResult>;
219
+ /**
220
+ * Manually compact the conversation via Claude's native `/compact` slash
221
+ * command. Custom focus instructions are forwarded as the command argument.
222
+ * No assistant message is emitted; the caller renders the confirmation.
223
+ *
224
+ * @param {import("../shared.js").CompactRequest} [request]
225
+ * @param {{ onProgress?: Function }} [options]
226
+ * @returns {Promise<import("../shared.js").CompactResult>}
227
+ */
228
+ runCompact(request?: import("../shared.js").CompactRequest, { onProgress }?: {
229
+ onProgress?: Function;
230
+ }): Promise<import("../shared.js").CompactResult>;
207
231
  close(): Promise<void>;
208
232
  }
209
233
  import { EventEmitter } from "node:events";
@@ -3,7 +3,7 @@ import { CLAUDE_AGENT_SDK_VARIANT as CLAUDE_PROVIDER_VARIANT } from "../built-in
3
3
  import { appendContextFilesToPrompt } from "../context-files.js";
4
4
  import { PROVIDER_MEDIA_CAPABILITIES, buildClaudeContent } from "../media-adapters.js";
5
5
  import { assertMediaCapabilities, defaultPromptForMedia, resolveTurnMedia, } from "../media-input.js";
6
- import { emitLog, extractLongFlagFromCommandLine, getBoundedEnvInt, isGoalStatus, loadEnvConfig, normalizeLogger, proxyToEnv, sanitizeForLog, } from "../shared.js";
6
+ import { emitLog, extractLongFlagFromCommandLine, getBoundedEnvInt, isGoalStatus, loadEnvConfig, normalizeLogger, noteToolFinished, noteToolStarted, proxyToEnv, sanitizeForLog, withActiveTool, } from "../shared.js";
7
7
  const DEFAULT_TURN_DEADLINE_MS = 12 * 60 * 1000;
8
8
  const MIN_TURN_DEADLINE_MS = 30 * 1000;
9
9
  const MAX_TURN_DEADLINE_MS = 30 * 60 * 1000;
@@ -32,6 +32,22 @@ function createTurnError(message, extras = {}) {
32
32
  }
33
33
  return error;
34
34
  }
35
+ /** Per-turn usage summed from streamed assistant messages (one entry per API response). */
36
+ function sumStreamedUsage(usageByMessageId) {
37
+ if (!usageByMessageId.size) {
38
+ return null;
39
+ }
40
+ const total = {};
41
+ for (const usage of usageByMessageId.values()) {
42
+ for (const key of ["input_tokens", "cache_creation_input_tokens", "cache_read_input_tokens", "output_tokens"]) {
43
+ const value = Number(usage?.[key]);
44
+ if (Number.isFinite(value)) {
45
+ total[key] = (total[key] || 0) + value;
46
+ }
47
+ }
48
+ }
49
+ return total;
50
+ }
35
51
  function normalizeClaudeBackend(backend) {
36
52
  const normalized = String(backend || "").trim().toLowerCase();
37
53
  if (normalized === "claude-code") {
@@ -135,9 +151,11 @@ export function resolveClaudeCommandForRoot(commandLine, env = process.env) {
135
151
  function normalizeText(value) {
136
152
  return typeof value === "string" ? value : "";
137
153
  }
154
+ function contentBlocks(message) {
155
+ return Array.isArray(message?.content) ? message.content : [];
156
+ }
138
157
  function extractAssistantText(message) {
139
- const content = Array.isArray(message?.content) ? message.content : [];
140
- return content
158
+ return contentBlocks(message)
141
159
  .map((block) => (block?.type === "text" && typeof block.text === "string" ? block.text : ""))
142
160
  .filter(Boolean)
143
161
  .join("");
@@ -212,9 +230,10 @@ function sanitizeSummary(value, maxLen = 180) {
212
230
  export class ClaudeAgentSdkSession extends EventEmitter {
213
231
  // Capability advertised via getSnapshot().capabilities so worker proxies
214
232
  // can short-circuit runGoal without an IPC round trip. Claude exposes
215
- // native `/goal` slash command, so this is true.
233
+ // native `/goal` and `/compact` slash commands, so both are true.
216
234
  static capabilities = Object.freeze({
217
235
  goal: true,
236
+ compact: true,
218
237
  media: PROVIDER_MEDIA_CAPABILITIES[CLAUDE_PROVIDER_VARIANT],
219
238
  });
220
239
  getCapabilities() {
@@ -339,7 +358,7 @@ export class ClaudeAgentSdkSession extends EventEmitter {
339
358
  return this.sessionInfo ? { ...this.sessionInfo } : null;
340
359
  }
341
360
  getCurrentTurnStatus() {
342
- return this.currentTurnStatus ? { ...this.currentTurnStatus } : null;
361
+ return withActiveTool(this.currentTurnStatus, this.currentTurn);
343
362
  }
344
363
  async ensureSessionInfo() {
345
364
  return this.getSessionInfo();
@@ -718,6 +737,16 @@ export class ClaudeAgentSdkSession extends EventEmitter {
718
737
  this.updateSessionInfo(message.session_id || message.sessionId);
719
738
  return;
720
739
  }
740
+ if (message.subtype === "compact_boundary") {
741
+ currentTurn.compactMetadata =
742
+ message.compact_metadata && typeof message.compact_metadata === "object"
743
+ ? { ...message.compact_metadata }
744
+ : {};
745
+ return;
746
+ }
747
+ if (message.subtype === "status" && message.compact_result === "failed") {
748
+ currentTurn.compactError = normalizeText(message.compact_error) || "compaction failed";
749
+ }
721
750
  if (message.subtype === "status" && message.status === "compacting") {
722
751
  await this.emitWorkingStatus({
723
752
  phase: "context_compaction",
@@ -773,12 +802,23 @@ export class ClaudeAgentSdkSession extends EventEmitter {
773
802
  }
774
803
  case "assistant": {
775
804
  this.updateSessionInfo(message.session_id || message.sessionId);
805
+ if (message.message?.id && message.message.usage) {
806
+ currentTurn.usageByMessageId.set(message.message.id, message.message.usage);
807
+ }
808
+ for (const block of contentBlocks(message.message)) {
809
+ if (block?.type === "tool_use") {
810
+ noteToolStarted(currentTurn, block.id, block.name, block.input);
811
+ }
812
+ }
776
813
  const text = extractAssistantText(message.message);
777
814
  if (!text) {
778
815
  return;
779
816
  }
780
- currentTurn.emittedAssistantMessage = true;
781
817
  currentTurn.fullText = text;
818
+ if (currentTurn.suppressReply) {
819
+ return;
820
+ }
821
+ currentTurn.emittedAssistantMessage = true;
782
822
  await this.emitWorkingStatus({
783
823
  phase: "message_aggregation",
784
824
  reply_in_progress: true,
@@ -788,6 +828,13 @@ export class ClaudeAgentSdkSession extends EventEmitter {
788
828
  await this.emitAssistantMessage(text);
789
829
  return;
790
830
  }
831
+ case "user":
832
+ for (const block of contentBlocks(message.message)) {
833
+ if (block?.type === "tool_result") {
834
+ noteToolFinished(currentTurn, block.tool_use_id);
835
+ }
836
+ }
837
+ return;
791
838
  case "rate_limit_event":
792
839
  this.updateSessionInfo(message.session_id || message.sessionId);
793
840
  this.rateLimitInfo =
@@ -829,7 +876,7 @@ export class ClaudeAgentSdkSession extends EventEmitter {
829
876
  }
830
877
  return true;
831
878
  }
832
- async runTurn(promptText, { useInitialImages = false, media: mediaInput, contextFiles, onProgress = null, jsonSchema = null } = {}) {
879
+ async runTurn(promptText, { useInitialImages = false, media: mediaInput, contextFiles, onProgress = null, jsonSchema = null, suppressReply = false } = {}) {
833
880
  if (this.closeRequested) {
834
881
  throw this.createSessionClosedError();
835
882
  }
@@ -855,16 +902,22 @@ export class ClaudeAgentSdkSession extends EventEmitter {
855
902
  if (!sdkModule || typeof sdkModule.query !== "function") {
856
903
  throw new Error("Claude Agent SDK is unavailable");
857
904
  }
858
- this.history.push({ role: "user", content: promptText });
905
+ if (!suppressReply) {
906
+ this.history.push({ role: "user", content: promptText });
907
+ }
859
908
  const abortController = new AbortController();
860
909
  const currentTurn = {
861
910
  abortController,
911
+ suppressReply,
912
+ compactMetadata: null,
913
+ compactError: "",
862
914
  emittedAssistantMessage: false,
863
915
  fullText: "",
864
916
  items: [],
865
917
  query: null,
866
918
  resultMessage: null,
867
919
  terminalWorkingStatusEmitted: false,
920
+ usageByMessageId: new Map(),
868
921
  };
869
922
  this.currentTurn = currentTurn;
870
923
  this.markTurnStartedStatus();
@@ -920,6 +973,8 @@ export class ClaudeAgentSdkSession extends EventEmitter {
920
973
  permissionDenials: Array.isArray(resultMessage.permission_denials)
921
974
  ? [...resultMessage.permission_denials]
922
975
  : [],
976
+ // A failed turn still spent tokens.
977
+ usage: resultMessage.usage ? { ...resultMessage.usage } : null,
923
978
  });
924
979
  }
925
980
  const structuredOutput = jsonSchema && resultMessage.structured_output && typeof resultMessage.structured_output === "object"
@@ -930,10 +985,10 @@ export class ClaudeAgentSdkSession extends EventEmitter {
930
985
  : normalizeText(resultMessage.result) ||
931
986
  currentTurn.fullText ||
932
987
  extractAssistantText(currentTurn.items.find((item) => item?.type === "assistant")?.message);
933
- if (!currentTurn.emittedAssistantMessage && responseText) {
988
+ if (!suppressReply && !currentTurn.emittedAssistantMessage && responseText) {
934
989
  await this.emitAssistantMessage(responseText);
935
990
  }
936
- if (responseText) {
991
+ if (!suppressReply && responseText) {
937
992
  this.history.push({ role: "assistant", content: responseText });
938
993
  }
939
994
  this.manualResumeReady = Boolean(this.sessionId);
@@ -957,9 +1012,14 @@ export class ClaudeAgentSdkSession extends EventEmitter {
957
1012
  modelUsage: resultMessage.modelUsage ? { ...resultMessage.modelUsage } : undefined,
958
1013
  structuredOutput: structuredOutput !== null ? structuredOutput : undefined,
959
1014
  },
1015
+ compactMetadata: currentTurn.compactMetadata || undefined,
1016
+ compactError: currentTurn.compactError || undefined,
960
1017
  };
961
1018
  }
962
1019
  catch (error) {
1020
+ // A failed turn still spent tokens; an interrupted query ends without a
1021
+ // result, so fall back to the usage streamed so far.
1022
+ const usage = error?.usage ?? sumStreamedUsage(currentTurn.usageByMessageId);
963
1023
  if (error?.reason === "turn_timeout") {
964
1024
  await this.interruptCurrentTurn();
965
1025
  }
@@ -971,9 +1031,12 @@ export class ClaudeAgentSdkSession extends EventEmitter {
971
1031
  }, onProgress);
972
1032
  }
973
1033
  if (this.closeRequested && error?.reason !== "session_closed") {
974
- throw this.createSessionClosedError();
1034
+ throw Object.assign(this.createSessionClosedError(), { usage });
975
1035
  }
976
1036
  this.maybeEmitAuthRequired(error?.message || "", error?.message || "");
1037
+ if (error && typeof error === "object") {
1038
+ error.usage = usage;
1039
+ }
977
1040
  throw error;
978
1041
  }
979
1042
  finally {
@@ -1099,6 +1162,46 @@ export class ClaudeAgentSdkSession extends EventEmitter {
1099
1162
  },
1100
1163
  };
1101
1164
  }
1165
+ /**
1166
+ * Manually compact the conversation via Claude's native `/compact` slash
1167
+ * command. Custom focus instructions are forwarded as the command argument.
1168
+ * No assistant message is emitted; the caller renders the confirmation.
1169
+ *
1170
+ * @param {import("../shared.js").CompactRequest} [request]
1171
+ * @param {{ onProgress?: Function }} [options]
1172
+ * @returns {Promise<import("../shared.js").CompactResult>}
1173
+ */
1174
+ async runCompact(request = {}, { onProgress = null } = {}) {
1175
+ const instructions = normalizeText(request?.instructions).trim();
1176
+ if (!this.sessionId || this.pendingHistorySeed) {
1177
+ return { compact: { status: "noop", instructionsApplied: false }, usage: null, metadata: {} };
1178
+ }
1179
+ const turnResult = await this.runTurn(instructions ? `/compact ${instructions}` : "/compact", {
1180
+ onProgress,
1181
+ suppressReply: true,
1182
+ });
1183
+ if (/not enough messages/i.test(turnResult.compactError || "")) {
1184
+ return { compact: { status: "noop", instructionsApplied: false }, usage: turnResult.usage, metadata: turnResult.metadata };
1185
+ }
1186
+ if (turnResult.compactError) {
1187
+ throw createTurnError(`Claude compaction failed: ${turnResult.compactError}`, {
1188
+ reason: "compact_failed",
1189
+ });
1190
+ }
1191
+ const boundary = turnResult.compactMetadata;
1192
+ const preTokens = Number(boundary?.pre_tokens);
1193
+ const postTokens = Number(boundary?.post_tokens);
1194
+ return {
1195
+ compact: {
1196
+ status: boundary ? "compacted" : "noop",
1197
+ instructionsApplied: Boolean(boundary && instructions),
1198
+ preTokens: Number.isFinite(preTokens) ? preTokens : undefined,
1199
+ postTokens: Number.isFinite(postTokens) ? postTokens : undefined,
1200
+ },
1201
+ usage: turnResult.usage,
1202
+ metadata: turnResult.metadata,
1203
+ };
1204
+ }
1102
1205
  async close() {
1103
1206
  if (this.closed) {
1104
1207
  return;
@@ -31,6 +31,8 @@ export class CodexAppServerSession extends EventEmitter<[never]> {
31
31
  nativeSessionId: string;
32
32
  rateLimits: any;
33
33
  tokenUsage: any;
34
+ turnTokenBaseline: number | null;
35
+ turnTokensSeen: boolean;
34
36
  currentTurnStatus: any;
35
37
  currentTurnActivityAt: number;
36
38
  now: any;
@@ -42,6 +44,14 @@ export class CodexAppServerSession extends EventEmitter<[never]> {
42
44
  activeAssistantMessageText: string;
43
45
  resolve: null;
44
46
  reject: null;
47
+ } | {
48
+ turnId: string;
49
+ fullText: string;
50
+ activeAssistantMessageId: string;
51
+ activeAssistantMessageText: string;
52
+ resolve: null;
53
+ reject: null;
54
+ suppressReply: boolean;
45
55
  } | {
46
56
  turnId: string;
47
57
  fullText: string;
@@ -127,6 +137,15 @@ export class CodexAppServerSession extends EventEmitter<[never]> {
127
137
  resumeReady?: boolean | undefined;
128
138
  }): void;
129
139
  applySessionConfigured(params: any): void;
140
+ /** Start counting a turn's tokens from the thread total known right now. */
141
+ beginTurnTokenAccounting(): void;
142
+ /**
143
+ * Thread token usage plus `turnTotalTokens`: tokens spent since the current
144
+ * turn (or goal) started. Codex only reports thread-cumulative totals, and
145
+ * only when a response completes, so a turn that ended before any response
146
+ * did has unknown usage.
147
+ */
148
+ snapshotTokenUsage(): any;
130
149
  resolveContextUsagePercent(): number | undefined;
131
150
  /**
132
151
  * Whether a turn failure was caused by the provider thread outgrowing the
@@ -175,6 +194,14 @@ export class CodexAppServerSession extends EventEmitter<[never]> {
175
194
  activeAssistantMessageText: string;
176
195
  resolve: null;
177
196
  reject: null;
197
+ } | {
198
+ turnId: string;
199
+ fullText: string;
200
+ activeAssistantMessageId: string;
201
+ activeAssistantMessageText: string;
202
+ resolve: null;
203
+ reject: null;
204
+ suppressReply: boolean;
178
205
  } | {
179
206
  turnId: string;
180
207
  fullText: string;
@@ -207,6 +234,14 @@ export class CodexAppServerSession extends EventEmitter<[never]> {
207
234
  activeAssistantMessageText: string;
208
235
  resolve: null;
209
236
  reject: null;
237
+ } | {
238
+ turnId: string;
239
+ fullText: string;
240
+ activeAssistantMessageId: string;
241
+ activeAssistantMessageText: string;
242
+ resolve: null;
243
+ reject: null;
244
+ suppressReply: boolean;
210
245
  } | {
211
246
  turnId: string;
212
247
  fullText: string;
@@ -244,6 +279,15 @@ export class CodexAppServerSession extends EventEmitter<[never]> {
244
279
  useInitialImages?: boolean | undefined;
245
280
  jsonSchema?: null | undefined;
246
281
  }): any;
282
+ /**
283
+ * Manually compact the thread via `thread/compact/start`. The app-server
284
+ * runs compaction as a regular turn, so completion is the usual
285
+ * `turn/completed`. The protocol takes no focus instructions.
286
+ *
287
+ * @param {import("../shared.js").CompactRequest} [request]
288
+ * @returns {Promise<import("../shared.js").CompactResult>}
289
+ */
290
+ runCompact(request?: import("../shared.js").CompactRequest): Promise<import("../shared.js").CompactResult>;
247
291
  /**
248
292
  * Normalize a goal payload from a codex `thread/goal/*` notification into
249
293
  * the cross-layer {@link GoalState} shape.