@bubblebrain-ai/bubble 0.0.13 → 0.0.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/dist/agent/execution-governor.js +1 -1
  2. package/dist/agent/tool-intent.js +1 -0
  3. package/dist/agent.d.ts +2 -0
  4. package/dist/agent.js +589 -316
  5. package/dist/approval/controller.d.ts +1 -0
  6. package/dist/approval/controller.js +20 -3
  7. package/dist/approval/tool-helper.js +2 -0
  8. package/dist/approval/types.d.ts +14 -1
  9. package/dist/context/compact.js +9 -3
  10. package/dist/context/projector.js +27 -12
  11. package/dist/debug-trace.d.ts +27 -0
  12. package/dist/debug-trace.js +385 -0
  13. package/dist/feishu/agent-host/approval-card.js +9 -0
  14. package/dist/feishu/serve.js +7 -1
  15. package/dist/main.js +28 -0
  16. package/dist/model-catalog.js +1 -0
  17. package/dist/orchestrator/default-hooks.js +19 -8
  18. package/dist/orchestrator/hooks.d.ts +1 -0
  19. package/dist/prompt/environment.js +2 -0
  20. package/dist/prompt/reminders.d.ts +5 -6
  21. package/dist/prompt/reminders.js +8 -9
  22. package/dist/prompt/runtime.js +2 -2
  23. package/dist/provider-openai-codex.d.ts +7 -0
  24. package/dist/provider-openai-codex.js +265 -124
  25. package/dist/provider-registry.d.ts +2 -0
  26. package/dist/provider-registry.js +58 -9
  27. package/dist/provider.d.ts +3 -0
  28. package/dist/provider.js +5 -1
  29. package/dist/session-log.js +13 -1
  30. package/dist/slash-commands/commands.js +12 -0
  31. package/dist/slash-commands/types.d.ts +2 -0
  32. package/dist/stats/usage.d.ts +52 -0
  33. package/dist/stats/usage.js +414 -0
  34. package/dist/tools/apply-patch.d.ts +9 -0
  35. package/dist/tools/apply-patch.js +330 -0
  36. package/dist/tools/bash.js +205 -44
  37. package/dist/tools/edit-apply.d.ts +5 -2
  38. package/dist/tools/edit-apply.js +221 -31
  39. package/dist/tools/edit.js +12 -3
  40. package/dist/tools/file-mutation-queue.d.ts +1 -0
  41. package/dist/tools/file-mutation-queue.js +12 -1
  42. package/dist/tools/index.d.ts +2 -0
  43. package/dist/tools/index.js +7 -1
  44. package/dist/tools/patch-apply.d.ts +41 -0
  45. package/dist/tools/patch-apply.js +312 -0
  46. package/dist/tools/server-manager.d.ts +36 -0
  47. package/dist/tools/server-manager.js +234 -0
  48. package/dist/tools/server.d.ts +6 -0
  49. package/dist/tools/server.js +245 -0
  50. package/dist/tools/write.d.ts +3 -6
  51. package/dist/tools/write.js +26 -46
  52. package/dist/tui/display-history.d.ts +1 -0
  53. package/dist/tui/display-history.js +5 -4
  54. package/dist/tui/edit-diff.js +6 -1
  55. package/dist/tui/model-picker-data.d.ts +10 -0
  56. package/dist/tui/model-picker-data.js +32 -0
  57. package/dist/tui/run.js +632 -89
  58. package/dist/tui/tool-renderers/fallback.js +1 -1
  59. package/dist/tui/tool-renderers/write-preview.js +2 -0
  60. package/dist/tui/trace-groups.js +10 -3
  61. package/dist/tui-ink/app.js +1 -4
  62. package/dist/tui-ink/approval/approval-dialog.js +7 -1
  63. package/dist/tui-ink/display-history.d.ts +1 -0
  64. package/dist/tui-ink/display-history.js +5 -4
  65. package/dist/tui-ink/message-list.js +14 -8
  66. package/dist/tui-ink/trace-groups.js +1 -1
  67. package/dist/tui-opentui/app.js +2 -0
  68. package/dist/tui-opentui/approval/approval-dialog.js +7 -1
  69. package/dist/tui-opentui/display-history.d.ts +1 -0
  70. package/dist/tui-opentui/display-history.js +5 -4
  71. package/dist/tui-opentui/edit-diff.js +6 -1
  72. package/dist/tui-opentui/message-list.js +6 -3
  73. package/dist/tui-opentui/trace-groups.js +10 -3
  74. package/dist/types.d.ts +12 -2
  75. package/package.json +1 -1
@@ -45,4 +45,5 @@ export declare class PermissionAwareApprovalController implements ApprovalContro
45
45
  checkRules(query: PermissionQuery): PermissionCheckResult;
46
46
  request(req: ApprovalRequest): Promise<ApprovalDecision>;
47
47
  private requestToQuery;
48
+ private checkRequestRules;
48
49
  }
@@ -26,8 +26,7 @@ export class PermissionAwareApprovalController {
26
26
  return checkPermission(ruleSet, query);
27
27
  }
28
28
  async request(req) {
29
- const query = this.requestToQuery(req);
30
- const ruleResult = this.checkRules(query);
29
+ const ruleResult = this.checkRequestRules(req);
31
30
  if (ruleResult.decision === "deny") {
32
31
  return {
33
32
  action: "reject",
@@ -38,7 +37,7 @@ export class PermissionAwareApprovalController {
38
37
  if (mode === "bypassPermissions") {
39
38
  return { action: "approve" };
40
39
  }
41
- if (mode === "default" && (req.type === "edit" || req.type === "write")) {
40
+ if (mode === "default" && (req.type === "edit" || req.type === "write" || req.type === "patch")) {
42
41
  return { action: "approve" };
43
42
  }
44
43
  if (mode === "plan") {
@@ -71,8 +70,26 @@ export class PermissionAwareApprovalController {
71
70
  return { tool: "Write", path: req.path, cwd: this.options.cwd };
72
71
  case "edit":
73
72
  return { tool: "Edit", path: req.path, cwd: this.options.cwd };
73
+ case "patch":
74
+ return { tool: "Edit", path: req.path, cwd: this.options.cwd };
74
75
  case "lsp":
75
76
  return { tool: "Lsp", path: req.path, cwd: this.options.cwd };
76
77
  }
77
78
  }
79
+ checkRequestRules(req) {
80
+ if (req.type !== "patch")
81
+ return this.checkRules(this.requestToQuery(req));
82
+ const perFile = req.files.map((file) => this.checkRules({
83
+ tool: file.kind === "add" ? "Write" : "Edit",
84
+ path: file.path,
85
+ cwd: this.options.cwd,
86
+ }));
87
+ const denied = perFile.find((result) => result.decision === "deny");
88
+ if (denied)
89
+ return denied;
90
+ if (perFile.length > 0 && perFile.every((result) => result.decision === "allow")) {
91
+ return { decision: "allow", rule: perFile[0].rule };
92
+ }
93
+ return { decision: "ask" };
94
+ }
78
95
  }
@@ -24,6 +24,8 @@ function approvalRequestLabel(req) {
24
24
  return `Edit to ${req.path}`;
25
25
  case "write":
26
26
  return `${req.fileExists ? "Overwrite" : "Write"} to ${req.path}`;
27
+ case "patch":
28
+ return `Patch to ${req.path}`;
27
29
  case "bash":
28
30
  return `Bash command \`${req.command}\``;
29
31
  case "lsp":
@@ -22,6 +22,19 @@ export interface WriteApprovalRequest {
22
22
  diff?: string;
23
23
  fileExists: boolean;
24
24
  }
25
+ export interface PatchApprovalRequest {
26
+ type: "patch";
27
+ /** Human-readable path summary for compact UIs. */
28
+ path: string;
29
+ /** All absolute paths touched by the patch. */
30
+ paths: string[];
31
+ files: Array<{
32
+ path: string;
33
+ kind: "add" | "update" | "delete";
34
+ }>;
35
+ /** Combined unified diff for all file changes. */
36
+ diff: string;
37
+ }
25
38
  export interface BashApprovalRequest {
26
39
  type: "bash";
27
40
  command: string;
@@ -32,7 +45,7 @@ export interface LspApprovalRequest {
32
45
  path: string;
33
46
  operation: string;
34
47
  }
35
- export type ApprovalRequest = EditApprovalRequest | WriteApprovalRequest | BashApprovalRequest | LspApprovalRequest;
48
+ export type ApprovalRequest = EditApprovalRequest | WriteApprovalRequest | PatchApprovalRequest | BashApprovalRequest | LspApprovalRequest;
36
49
  export type ApprovalDecision = {
37
50
  action: "approve";
38
51
  feedback?: string;
@@ -255,9 +255,8 @@ function entriesToMessages(entries) {
255
255
  break;
256
256
  case "assistant_message":
257
257
  messages.push({
258
+ ...entry.message,
258
259
  role: "assistant",
259
- content: entry.message.content,
260
- reasoning: entry.message.reasoning,
261
260
  });
262
261
  break;
263
262
  case "tool_call": {
@@ -358,7 +357,14 @@ function findLatestSummaryIndex(entries) {
358
357
  return -1;
359
358
  }
360
359
  function nextSummaryId(entries) {
361
- return `${entries.length + 1}`;
360
+ let max = 0;
361
+ for (const entry of entries) {
362
+ const match = /^(\d+)/.exec(entry.id);
363
+ if (!match)
364
+ continue;
365
+ max = Math.max(max, Number(match[1]));
366
+ }
367
+ return `${max + 1}`;
362
368
  }
363
369
  function cloneMessage(message) {
364
370
  if (message.role === "assistant") {
@@ -1,29 +1,38 @@
1
1
  import { getContextBudget } from "./budget.js";
2
2
  import { compactCurrentTurnToolGroups, compactMessages } from "./compact.js";
3
3
  import { pruneMessages } from "./prune.js";
4
- // Prefix-cache invariant: every projected output starts with the concatenation
5
- // of (in order) system + meta messages from the input, followed by the
6
- // conversational body. Compactors (compactMessages, compactCurrentTurnToolGroups,
7
- // compactMessagesWithLLM, compactWithLLM) MUST preserve every existing
8
- // system/meta message in its original position so the cacheable prefix
9
- // stays byte-identical across turns where compaction didn't fire. Inserting
10
- // new dynamic content (summaries, etc.) AFTER system+meta is safe; inserting
11
- // it within or before them is not.
4
+ // Prefix-cache invariant: only the leading static system prompt is promoted to
5
+ // the first provider message. Runtime meta reminders stay in the conversational
6
+ // body at their original relative position, so a new per-turn reminder does not
7
+ // rewrite the cacheable prefix before the existing history.
12
8
  export function projectMessages(messages, options = {}) {
13
9
  const mode = options.mode ?? "full";
14
10
  const projectedBody = [];
15
11
  const systemContext = [];
12
+ let inLeadingSystemPrefix = true;
16
13
  for (const message of messages) {
17
- if (message.role === "system") {
14
+ if (message.role === "system" && inLeadingSystemPrefix) {
18
15
  systemContext.push(message.content);
19
16
  continue;
20
17
  }
21
18
  if (message.role === "meta") {
19
+ inLeadingSystemPrefix = false;
22
20
  if (message.includeInLlm !== false) {
23
- systemContext.push(formatMetaMessage(message));
21
+ projectedBody.push({
22
+ role: "user",
23
+ content: formatMetaMessage(message),
24
+ });
24
25
  }
25
26
  continue;
26
27
  }
28
+ inLeadingSystemPrefix = false;
29
+ if (message.role === "system") {
30
+ projectedBody.push({
31
+ role: "user",
32
+ content: formatRuntimeSystemMessage(message),
33
+ });
34
+ continue;
35
+ }
27
36
  if (message.role === "assistant" && isEmptyAssistantMessage(message)) {
28
37
  continue;
29
38
  }
@@ -147,9 +156,12 @@ export function repairToolCallChains(messages) {
147
156
  }
148
157
  function isEmptyAssistantMessage(message) {
149
158
  const hasContent = message.content.trim().length > 0;
150
- const hasReasoning = (message.reasoning ?? "").trim().length > 0;
151
159
  const hasToolCalls = !!message.toolCalls && message.toolCalls.length > 0;
152
- return !hasContent && !hasReasoning && !hasToolCalls;
160
+ // Reasoning-only assistant messages are not valid ChatCompletions history:
161
+ // providers require assistant history to contain user-visible content or
162
+ // tool_calls. Keep reasoning attached to real assistant/tool-call messages,
163
+ // but drop standalone thinking-only turns before provider projection.
164
+ return !hasContent && !hasToolCalls;
153
165
  }
154
166
  function formatMetaMessage(message) {
155
167
  switch (message.kind) {
@@ -160,6 +172,9 @@ function formatMetaMessage(message) {
160
172
  return `Runtime context:\n${message.content}`;
161
173
  }
162
174
  }
175
+ function formatRuntimeSystemMessage(message) {
176
+ return `Runtime context:\n${message.content}`;
177
+ }
163
178
  function cloneMessage(message) {
164
179
  if (message.role === "assistant") {
165
180
  return {
@@ -0,0 +1,27 @@
1
+ import type { AgentEvent, Message, ToolResult } from "./types.js";
2
+ export interface DebugTraceContext {
3
+ cwd?: string;
4
+ sessionFile?: string;
5
+ provider?: string;
6
+ model?: string;
7
+ renderer?: string;
8
+ surface?: string;
9
+ }
10
+ export interface DebugTraceInfo {
11
+ enabled: boolean;
12
+ path?: string;
13
+ runId?: string;
14
+ rawEnabled: boolean;
15
+ }
16
+ export declare function isDebugTraceEnabled(): boolean;
17
+ export declare function isDebugTraceRawEnabled(): boolean;
18
+ export declare function configureDebugTrace(context: DebugTraceContext): DebugTraceInfo;
19
+ export declare function getDebugTraceInfo(): DebugTraceInfo;
20
+ export declare function traceEvent(phase: string, detail?: Record<string, unknown>, context?: DebugTraceContext): void;
21
+ export declare function summarizeTraceText(value: unknown): Record<string, unknown> | undefined;
22
+ export declare function summarizeTraceValue(value: unknown): Record<string, unknown> | undefined;
23
+ export declare function summarizeTraceMessage(message: Message): Record<string, unknown>;
24
+ export declare function summarizeTraceToolResult(result: ToolResult): Record<string, unknown>;
25
+ export declare function summarizeTraceError(error: unknown): Record<string, unknown>;
26
+ export declare function summarizeAgentEventForTrace(event: AgentEvent): Record<string, unknown>;
27
+ export declare function resetDebugTraceForTests(): void;
@@ -0,0 +1,385 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { appendFileSync, mkdirSync, readdirSync, rmSync, statSync } from "node:fs";
3
+ import { dirname, isAbsolute, join } from "node:path";
4
+ import { performance } from "node:perf_hooks";
5
+ import { getBubbleHome } from "./bubble-home.js";
6
+ const TRACE_VERSION = 1;
7
+ const TRUE_VALUES = new Set(["1", "true", "yes", "on"]);
8
+ const FALSE_VALUES = new Set(["0", "false", "no", "off"]);
9
+ const DEFAULT_MAX_AGE_DAYS = 7;
10
+ const DEFAULT_RAW_MAX_BYTES = 256 * 1024;
11
+ let initialized = false;
12
+ let tracePath;
13
+ let runId;
14
+ let sequence = 0;
15
+ let startedAt = performance.now();
16
+ let baseContext = {};
17
+ let defaultPruneDone = false;
18
+ export function isDebugTraceEnabled() {
19
+ const value = process.env.BUBBLE_TRACE?.trim();
20
+ if (!value)
21
+ return false;
22
+ return !FALSE_VALUES.has(value.toLowerCase());
23
+ }
24
+ export function isDebugTraceRawEnabled() {
25
+ const value = process.env.BUBBLE_TRACE_RAW?.trim().toLowerCase();
26
+ return !!value && !FALSE_VALUES.has(value);
27
+ }
28
+ export function configureDebugTrace(context) {
29
+ baseContext = { ...baseContext, ...dropUndefined(context) };
30
+ const path = ensureTracePath();
31
+ if (!path)
32
+ return { enabled: false, rawEnabled: isDebugTraceRawEnabled() };
33
+ return {
34
+ enabled: true,
35
+ path,
36
+ runId,
37
+ rawEnabled: isDebugTraceRawEnabled(),
38
+ };
39
+ }
40
+ export function getDebugTraceInfo() {
41
+ const path = ensureTracePath();
42
+ if (!path)
43
+ return { enabled: false, rawEnabled: isDebugTraceRawEnabled() };
44
+ return {
45
+ enabled: true,
46
+ path,
47
+ runId,
48
+ rawEnabled: isDebugTraceRawEnabled(),
49
+ };
50
+ }
51
+ export function traceEvent(phase, detail, context) {
52
+ const path = ensureTracePath();
53
+ if (!path)
54
+ return;
55
+ const eventContext = {
56
+ ...baseContext,
57
+ ...dropUndefined(context ?? {}),
58
+ };
59
+ const line = {
60
+ traceVersion: TRACE_VERSION,
61
+ ts: new Date().toISOString(),
62
+ elapsedMs: Math.round(performance.now() - startedAt),
63
+ seq: ++sequence,
64
+ runId,
65
+ pid: process.pid,
66
+ phase,
67
+ ...eventContext,
68
+ ...(detail ? { detail: sanitizeTraceValue(detail) } : {}),
69
+ };
70
+ try {
71
+ appendFileSync(path, JSON.stringify(line) + "\n", "utf-8");
72
+ }
73
+ catch {
74
+ // Debug tracing must never affect normal agent execution.
75
+ }
76
+ }
77
+ export function summarizeTraceText(value) {
78
+ if (typeof value !== "string")
79
+ return summarizeTraceValue(value);
80
+ return summarizeString(value);
81
+ }
82
+ export function summarizeTraceValue(value) {
83
+ if (value === undefined)
84
+ return undefined;
85
+ if (value === null)
86
+ return { type: "null" };
87
+ if (typeof value === "string")
88
+ return summarizeString(value);
89
+ if (typeof value === "number" || typeof value === "boolean") {
90
+ return { type: typeof value, value };
91
+ }
92
+ if (Array.isArray(value)) {
93
+ return {
94
+ type: "array",
95
+ count: value.length,
96
+ json: summarizeJson(value),
97
+ };
98
+ }
99
+ if (typeof value === "object") {
100
+ return {
101
+ type: "object",
102
+ keys: Object.keys(value).slice(0, 32),
103
+ json: summarizeJson(value),
104
+ };
105
+ }
106
+ return { type: typeof value, value: String(value) };
107
+ }
108
+ export function summarizeTraceMessage(message) {
109
+ if (message.role === "assistant") {
110
+ return {
111
+ role: message.role,
112
+ content: summarizeTraceText(message.content),
113
+ reasoning: summarizeTraceText(message.reasoning ?? ""),
114
+ error: message.error,
115
+ toolCalls: message.toolCalls?.map((call) => ({
116
+ id: call.id,
117
+ name: call.name,
118
+ args: summarizeTraceText(call.arguments),
119
+ argsCorrupt: call.argsCorrupt,
120
+ })),
121
+ };
122
+ }
123
+ if (message.role === "tool") {
124
+ return {
125
+ role: message.role,
126
+ toolCallId: message.toolCallId,
127
+ content: summarizeTraceText(message.content),
128
+ isError: message.isError,
129
+ metadata: message.metadata,
130
+ };
131
+ }
132
+ if (message.role === "user") {
133
+ return {
134
+ role: message.role,
135
+ content: summarizeTraceValue(message.content),
136
+ };
137
+ }
138
+ return {
139
+ role: message.role,
140
+ kind: "kind" in message ? message.kind : undefined,
141
+ content: summarizeTraceText(message.content),
142
+ };
143
+ }
144
+ export function summarizeTraceToolResult(result) {
145
+ return {
146
+ content: summarizeTraceText(result.content),
147
+ isError: result.isError,
148
+ status: result.status,
149
+ metadata: result.metadata,
150
+ };
151
+ }
152
+ export function summarizeTraceError(error) {
153
+ if (error instanceof Error) {
154
+ return {
155
+ name: error.name,
156
+ message: error.message,
157
+ stack: truncateString(error.stack ?? "", 4000),
158
+ };
159
+ }
160
+ return {
161
+ name: "Error",
162
+ message: String(error),
163
+ };
164
+ }
165
+ export function summarizeAgentEventForTrace(event) {
166
+ switch (event.type) {
167
+ case "text_delta":
168
+ case "reasoning_delta":
169
+ return { type: event.type, content: summarizeTraceText(event.content) };
170
+ case "tool_call_start":
171
+ return { type: event.type, id: event.id, name: event.name };
172
+ case "tool_call_delta":
173
+ return {
174
+ type: event.type,
175
+ id: event.id,
176
+ name: event.name,
177
+ argumentsDelta: summarizeTraceText(event.argumentsDelta),
178
+ arguments: summarizeTraceText(event.arguments),
179
+ };
180
+ case "tool_call_end":
181
+ return {
182
+ type: event.type,
183
+ id: event.id,
184
+ name: event.name,
185
+ arguments: summarizeTraceText(event.arguments),
186
+ };
187
+ case "tool_start":
188
+ return {
189
+ type: event.type,
190
+ id: event.id,
191
+ name: event.name,
192
+ args: summarizeTraceValue(event.args),
193
+ };
194
+ case "tool_update":
195
+ return {
196
+ type: event.type,
197
+ id: event.id,
198
+ name: event.name,
199
+ update: summarizeTraceValue(event.update),
200
+ };
201
+ case "tool_end":
202
+ return {
203
+ type: event.type,
204
+ id: event.id,
205
+ name: event.name,
206
+ result: summarizeTraceToolResult(event.result),
207
+ };
208
+ case "turn_end":
209
+ return {
210
+ type: event.type,
211
+ usage: event.usage,
212
+ willContinue: event.willContinue,
213
+ };
214
+ case "input_applied":
215
+ case "input_rejected":
216
+ return {
217
+ ...event,
218
+ content: summarizeTraceText(event.content),
219
+ };
220
+ case "todos_updated":
221
+ return { type: event.type, count: event.todos.length };
222
+ default:
223
+ return { ...event };
224
+ }
225
+ }
226
+ export function resetDebugTraceForTests() {
227
+ initialized = false;
228
+ tracePath = undefined;
229
+ runId = undefined;
230
+ sequence = 0;
231
+ startedAt = performance.now();
232
+ baseContext = {};
233
+ defaultPruneDone = false;
234
+ }
235
+ function ensureTracePath() {
236
+ if (!isDebugTraceEnabled())
237
+ return undefined;
238
+ if (initialized)
239
+ return tracePath;
240
+ initialized = true;
241
+ startedAt = performance.now();
242
+ runId = resolveRunId();
243
+ tracePath = resolveTracePath(runId);
244
+ try {
245
+ mkdirSync(dirname(tracePath), { recursive: true });
246
+ pruneDefaultTraceDirs();
247
+ }
248
+ catch {
249
+ // The write path may still fail later; tracing remains best-effort.
250
+ }
251
+ return tracePath;
252
+ }
253
+ function resolveRunId() {
254
+ const explicit = process.env.BUBBLE_TRACE_RUN_ID?.trim();
255
+ if (explicit)
256
+ return sanitizeFileSegment(explicit);
257
+ const stamp = new Date().toISOString().replace(/[:.]/g, "-");
258
+ return `trace-${stamp}-${process.pid}-${randomUUID().slice(0, 8)}`;
259
+ }
260
+ function resolveTracePath(id) {
261
+ const explicitPath = process.env.BUBBLE_TRACE_PATH?.trim();
262
+ if (explicitPath)
263
+ return isAbsolute(explicitPath) ? explicitPath : join(process.cwd(), explicitPath);
264
+ const value = process.env.BUBBLE_TRACE?.trim();
265
+ if (value && !TRUE_VALUES.has(value.toLowerCase()) && !FALSE_VALUES.has(value.toLowerCase())) {
266
+ return isAbsolute(value) ? value : join(process.cwd(), value);
267
+ }
268
+ const dateKey = new Date().toISOString().slice(0, 10);
269
+ return join(getBubbleHome(), "debug-runs", dateKey, `${id}.jsonl`);
270
+ }
271
+ function pruneDefaultTraceDirs() {
272
+ if (defaultPruneDone || process.env.BUBBLE_TRACE_PATH?.trim())
273
+ return;
274
+ defaultPruneDone = true;
275
+ const maxAgeDays = Number(process.env.BUBBLE_TRACE_MAX_AGE_DAYS ?? DEFAULT_MAX_AGE_DAYS);
276
+ if (!Number.isFinite(maxAgeDays) || maxAgeDays <= 0)
277
+ return;
278
+ const root = join(getBubbleHome(), "debug-runs");
279
+ const cutoff = Date.now() - maxAgeDays * 24 * 60 * 60 * 1000;
280
+ let entries;
281
+ try {
282
+ entries = readdirSync(root);
283
+ }
284
+ catch {
285
+ return;
286
+ }
287
+ for (const entry of entries) {
288
+ const path = join(root, entry);
289
+ try {
290
+ const stat = statSync(path);
291
+ if (stat.mtimeMs < cutoff)
292
+ rmSync(path, { recursive: true, force: true });
293
+ }
294
+ catch {
295
+ // Ignore cleanup failures.
296
+ }
297
+ }
298
+ }
299
+ function summarizeString(value) {
300
+ const byteLength = Buffer.byteLength(value, "utf8");
301
+ const summary = {
302
+ type: "string",
303
+ chars: value.length,
304
+ bytes: byteLength,
305
+ hash: hashString(value),
306
+ };
307
+ if (isDebugTraceRawEnabled()) {
308
+ summary.raw = truncateRaw(value, byteLength);
309
+ }
310
+ return summary;
311
+ }
312
+ function summarizeJson(value) {
313
+ const json = safeJsonStringify(value);
314
+ if (json === undefined)
315
+ return { serializable: false };
316
+ const byteLength = Buffer.byteLength(json, "utf8");
317
+ const summary = {
318
+ serializable: true,
319
+ chars: json.length,
320
+ bytes: byteLength,
321
+ hash: hashString(json),
322
+ };
323
+ if (isDebugTraceRawEnabled()) {
324
+ summary.raw = truncateRaw(json, byteLength);
325
+ }
326
+ return summary;
327
+ }
328
+ function sanitizeTraceValue(value) {
329
+ if (value === undefined)
330
+ return undefined;
331
+ if (value === null)
332
+ return null;
333
+ if (typeof value === "string")
334
+ return value;
335
+ if (typeof value === "number" || typeof value === "boolean")
336
+ return value;
337
+ if (Array.isArray(value))
338
+ return value.map((item) => sanitizeTraceValue(item));
339
+ if (typeof value === "object") {
340
+ const out = {};
341
+ for (const [key, item] of Object.entries(value)) {
342
+ if (item !== undefined)
343
+ out[key] = sanitizeTraceValue(item);
344
+ }
345
+ return out;
346
+ }
347
+ return String(value);
348
+ }
349
+ function truncateRaw(value, byteLength) {
350
+ const limit = Number(process.env.BUBBLE_TRACE_RAW_MAX_BYTES ?? DEFAULT_RAW_MAX_BYTES);
351
+ if (!Number.isFinite(limit) || limit <= 0 || byteLength <= limit)
352
+ return value;
353
+ return {
354
+ value: value.slice(0, Math.max(0, limit)),
355
+ truncated: true,
356
+ bytes: byteLength,
357
+ };
358
+ }
359
+ function truncateString(value, maxChars) {
360
+ if (!value)
361
+ return undefined;
362
+ return value.length > maxChars ? `${value.slice(0, maxChars)}...` : value;
363
+ }
364
+ function hashString(value) {
365
+ return createHash("sha256").update(value).digest("hex").slice(0, 16);
366
+ }
367
+ function safeJsonStringify(value) {
368
+ try {
369
+ return JSON.stringify(value);
370
+ }
371
+ catch {
372
+ return undefined;
373
+ }
374
+ }
375
+ function dropUndefined(value) {
376
+ const out = {};
377
+ for (const [key, item] of Object.entries(value)) {
378
+ if (item !== undefined)
379
+ out[key] = item;
380
+ }
381
+ return out;
382
+ }
383
+ function sanitizeFileSegment(value) {
384
+ return value.replace(/[^A-Za-z0-9._-]/g, "_").slice(0, 120) || "trace";
385
+ }
@@ -32,6 +32,15 @@ export function formatApprovalRequest(req) {
32
32
  `\n**diff:**\n\`\`\`diff\n${truncate(req.diff, DIFF_PREVIEW_MAX)}\n\`\`\``,
33
33
  ].join("\n"),
34
34
  };
35
+ case "patch":
36
+ return {
37
+ title: "应用补丁",
38
+ body: [
39
+ `**files:** ${req.paths.length}`,
40
+ `**path:** \`${truncate(req.path, PATH_PREVIEW_MAX)}\``,
41
+ `\n**diff:**\n\`\`\`diff\n${truncate(req.diff, DIFF_PREVIEW_MAX)}\n\`\`\``,
42
+ ].join("\n"),
43
+ };
35
44
  case "lsp":
36
45
  return {
37
46
  title: `LSP 操作 (${req.operation})`,
@@ -91,7 +91,13 @@ export async function serveFeishu(opts = {}) {
91
91
  if (mcpLoaded.servers.length > 0) {
92
92
  await mcpManager.start();
93
93
  }
94
- const createProvider = (providerId, apiKey, baseURL, promptCacheKey) => createProviderInstance({ providerId, apiKey, baseURL, promptCacheKey });
94
+ const createProvider = (providerId, apiKey, baseURL, promptCacheKey) => createProviderInstance({
95
+ providerId,
96
+ apiKey,
97
+ baseURL,
98
+ promptCacheKey,
99
+ openAICodexAuth: providerRegistry.createOpenAICodexAuthAdapter(providerId),
100
+ });
95
101
  const createProviderForRoute = async (route, promptCacheKey) => {
96
102
  const target = providerRegistry.getConfigured().find((p) => p.id === route.providerId);
97
103
  if (!target?.apiKey) {