@bubblebrain-ai/bubble 0.0.13 → 0.0.15

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 (80) 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/cli.d.ts +3 -1
  10. package/dist/cli.js +12 -0
  11. package/dist/context/compact.js +9 -3
  12. package/dist/context/projector.js +27 -12
  13. package/dist/debug-trace.d.ts +27 -0
  14. package/dist/debug-trace.js +385 -0
  15. package/dist/feishu/agent-host/approval-card.js +9 -0
  16. package/dist/feishu/serve.js +7 -1
  17. package/dist/main.js +41 -0
  18. package/dist/model-catalog.js +1 -0
  19. package/dist/orchestrator/default-hooks.js +19 -8
  20. package/dist/orchestrator/hooks.d.ts +1 -0
  21. package/dist/prompt/environment.js +2 -0
  22. package/dist/prompt/reminders.d.ts +5 -6
  23. package/dist/prompt/reminders.js +8 -9
  24. package/dist/prompt/runtime.js +2 -2
  25. package/dist/provider-openai-codex.d.ts +7 -0
  26. package/dist/provider-openai-codex.js +265 -124
  27. package/dist/provider-registry.d.ts +2 -0
  28. package/dist/provider-registry.js +58 -9
  29. package/dist/provider.d.ts +3 -0
  30. package/dist/provider.js +5 -1
  31. package/dist/session-log.js +13 -1
  32. package/dist/slash-commands/commands.js +12 -0
  33. package/dist/slash-commands/types.d.ts +2 -0
  34. package/dist/stats/usage.d.ts +52 -0
  35. package/dist/stats/usage.js +414 -0
  36. package/dist/tools/apply-patch.d.ts +9 -0
  37. package/dist/tools/apply-patch.js +330 -0
  38. package/dist/tools/bash.js +205 -44
  39. package/dist/tools/edit-apply.d.ts +5 -2
  40. package/dist/tools/edit-apply.js +221 -31
  41. package/dist/tools/edit.js +12 -3
  42. package/dist/tools/file-mutation-queue.d.ts +1 -0
  43. package/dist/tools/file-mutation-queue.js +12 -1
  44. package/dist/tools/index.d.ts +2 -0
  45. package/dist/tools/index.js +7 -1
  46. package/dist/tools/patch-apply.d.ts +41 -0
  47. package/dist/tools/patch-apply.js +312 -0
  48. package/dist/tools/server-manager.d.ts +36 -0
  49. package/dist/tools/server-manager.js +234 -0
  50. package/dist/tools/server.d.ts +6 -0
  51. package/dist/tools/server.js +245 -0
  52. package/dist/tools/write.d.ts +3 -6
  53. package/dist/tools/write.js +26 -46
  54. package/dist/tui/display-history.d.ts +1 -0
  55. package/dist/tui/display-history.js +5 -4
  56. package/dist/tui/edit-diff.js +6 -1
  57. package/dist/tui/model-picker-data.d.ts +10 -0
  58. package/dist/tui/model-picker-data.js +32 -0
  59. package/dist/tui/run.d.ts +2 -0
  60. package/dist/tui/run.js +717 -122
  61. package/dist/tui/tool-renderers/fallback.js +1 -1
  62. package/dist/tui/tool-renderers/write-preview.js +2 -0
  63. package/dist/tui/trace-groups.js +10 -3
  64. package/dist/tui-ink/app.js +1 -4
  65. package/dist/tui-ink/approval/approval-dialog.js +7 -1
  66. package/dist/tui-ink/display-history.d.ts +1 -0
  67. package/dist/tui-ink/display-history.js +5 -4
  68. package/dist/tui-ink/message-list.js +14 -8
  69. package/dist/tui-ink/trace-groups.js +1 -1
  70. package/dist/tui-opentui/app.js +2 -0
  71. package/dist/tui-opentui/approval/approval-dialog.js +7 -1
  72. package/dist/tui-opentui/display-history.d.ts +1 -0
  73. package/dist/tui-opentui/display-history.js +5 -4
  74. package/dist/tui-opentui/edit-diff.js +6 -1
  75. package/dist/tui-opentui/message-list.js +6 -3
  76. package/dist/tui-opentui/trace-groups.js +10 -3
  77. package/dist/types.d.ts +12 -2
  78. package/dist/update/index.d.ts +46 -0
  79. package/dist/update/index.js +240 -0
  80. 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;
package/dist/cli.d.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  * CLI argument parsing.
3
3
  */
4
4
  import type { PermissionMode, ThinkingLevel } from "./types.js";
5
- export type CliCommand = "default" | "serve";
5
+ export type CliCommand = "default" | "serve" | "update";
6
6
  export interface CliArgs {
7
7
  command: CliCommand;
8
8
  model?: string;
@@ -22,6 +22,8 @@ export interface CliArgs {
22
22
  killOld?: boolean;
23
23
  /** `serve` subcommand: connect then exit. */
24
24
  dryRun?: boolean;
25
+ /** `update` subcommand: only report whether an update exists, don't install. */
26
+ checkOnly?: boolean;
25
27
  }
26
28
  export declare function parseArgs(argv: string[]): CliArgs;
27
29
  export declare function printHelp(): void;
package/dist/cli.js CHANGED
@@ -14,6 +14,10 @@ export function parseArgs(argv) {
14
14
  args.command = "serve";
15
15
  startIndex = 1;
16
16
  }
17
+ else if (argv[0] === "update" || argv[0] === "upgrade") {
18
+ args.command = "update";
19
+ startIndex = 1;
20
+ }
17
21
  }
18
22
  for (let i = startIndex; i < argv.length; i++) {
19
23
  const arg = argv[i];
@@ -72,6 +76,9 @@ export function parseArgs(argv) {
72
76
  case "--dry-run":
73
77
  args.dryRun = true;
74
78
  break;
79
+ case "--check":
80
+ args.checkOnly = true;
81
+ break;
75
82
  default:
76
83
  if (!arg.startsWith("-") && !args.prompt) {
77
84
  args.prompt = arg;
@@ -85,6 +92,7 @@ export function printHelp() {
85
92
  console.log(`
86
93
  Usage:
87
94
  bubble [options] [prompt] Start interactive TUI
95
+ bubble update [--check] Update to the latest version (alias: upgrade)
88
96
  bubble serve --feishu [options] Run as a Feishu bot host
89
97
 
90
98
  Options (default):
@@ -99,8 +107,12 @@ Options (default):
99
107
  --dangerously-skip-permissions
100
108
  Enable bypass mode (auto-approve EVERY tool; disables all safety prompts)
101
109
  -p, --print Non-interactive mode (single prompt)
110
+ -v, --version Print the installed version and exit
102
111
  -h, --help Show this help
103
112
 
113
+ Options (update):
114
+ --check Only report whether a newer version exists
115
+
104
116
  Options (serve --feishu):
105
117
  --setup Force the wizard (scan QR + bind first scope)
106
118
  --kill-old Kill any conflicting bubble instance for the same App ID
@@ -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})`,