@mindstudio-ai/remy 0.1.266 → 0.1.267

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/dist/headless.js CHANGED
@@ -2082,6 +2082,12 @@ var setProjectMetadataTool = {
2082
2082
 
2083
2083
  // src/tools/common/compactConversation.ts
2084
2084
  var compactConversationTool = {
2085
+ backgroundOnly: true,
2086
+ // Silent: the completed summary is written onto the tool block for the UI
2087
+ // detail view only. The model must never receive it as a message — it gets
2088
+ // the summary the correct way, as the checkpoint prefix in every later API
2089
+ // call (cleanMessagesForApi).
2090
+ backgroundNotify: "silent",
2085
2091
  definition: {
2086
2092
  clearable: false,
2087
2093
  name: "compactConversation",
@@ -2095,11 +2101,32 @@ var compactConversationTool = {
2095
2101
  if (!context?.conversationMessages || !context.apiConfig) {
2096
2102
  return "Error: compaction requires execution context.";
2097
2103
  }
2104
+ const { toolCallId, onBackgroundComplete, toolRegistry } = context;
2098
2105
  triggerCompaction(
2099
2106
  { messages: context.conversationMessages },
2100
2107
  context.apiConfig,
2101
- { blocking: false, requestId: context.requestId }
2102
- ).catch(() => {
2108
+ // origin/toolCallId tell the headless listener a real block exists —
2109
+ // it must not synthesize a UI-only one for this compaction.
2110
+ {
2111
+ blocking: false,
2112
+ requestId: context.requestId,
2113
+ origin: "tool",
2114
+ toolCallId
2115
+ }
2116
+ ).then((summaries) => {
2117
+ onBackgroundComplete?.(
2118
+ toolCallId,
2119
+ "compactConversation",
2120
+ formatSummariesResult(summaries)
2121
+ );
2122
+ }).catch((err) => {
2123
+ onBackgroundComplete?.(
2124
+ toolCallId,
2125
+ "compactConversation",
2126
+ `Error: ${err.message || "Compaction failed"}`
2127
+ );
2128
+ }).finally(() => {
2129
+ toolRegistry?.unregister(toolCallId);
2103
2130
  });
2104
2131
  return "Compaction started in the background.";
2105
2132
  }
@@ -3385,16 +3412,55 @@ function capToolResult(result) {
3385
3412
  }
3386
3413
 
3387
3414
  // src/statusWatcher.ts
3415
+ var INTERNAL_PAYLOAD_MARKERS = [
3416
+ "[USER CANCELLED]",
3417
+ "[INTERRUPTED]",
3418
+ "[INTERRUPTED - PARTIAL OUTPUT RETRIEVED]",
3419
+ "<background_results>",
3420
+ "<tool_result"
3421
+ ];
3422
+ function sanitizeStatusText(text) {
3423
+ if (!text) {
3424
+ return "";
3425
+ }
3426
+ for (const marker of INTERNAL_PAYLOAD_MARKERS) {
3427
+ if (text.includes(marker)) {
3428
+ return "";
3429
+ }
3430
+ }
3431
+ const truncIdx = text.indexOf("(tool result truncated at");
3432
+ return truncIdx === -1 ? text : text.slice(0, truncIdx);
3433
+ }
3388
3434
  function startStatusWatcher(config) {
3389
3435
  const { apiConfig, getContext, onStatus, interval = 5e3, signal } = config;
3390
3436
  let inflight2 = false;
3391
3437
  let stopped = false;
3392
3438
  let pauseCount = 0;
3393
3439
  const url = `${apiConfig.baseUrl}/_internal/v2/agent/remy/generate-status`;
3440
+ let consecutiveFailures = 0;
3441
+ let backoffMs = 0;
3442
+ let nextAllowedAt = 0;
3443
+ const MAX_CONSECUTIVE_FAILURES = 10;
3444
+ const MAX_BACKOFF_MS = 6e4;
3445
+ function recordFailure() {
3446
+ consecutiveFailures++;
3447
+ backoffMs = Math.min(
3448
+ backoffMs === 0 ? interval : backoffMs * 2,
3449
+ MAX_BACKOFF_MS
3450
+ );
3451
+ nextAllowedAt = Date.now() + backoffMs;
3452
+ if (consecutiveFailures >= MAX_CONSECUTIVE_FAILURES && !stopped) {
3453
+ stopped = true;
3454
+ clearInterval(timer);
3455
+ }
3456
+ }
3394
3457
  async function tick() {
3395
3458
  if (stopped || signal?.aborted || inflight2 || pauseCount > 0) {
3396
3459
  return;
3397
3460
  }
3461
+ if (Date.now() < nextAllowedAt) {
3462
+ return;
3463
+ }
3398
3464
  inflight2 = true;
3399
3465
  try {
3400
3466
  const context = getContext();
@@ -3410,18 +3476,29 @@ function startStatusWatcher(config) {
3410
3476
  body: JSON.stringify({ appId: apiConfig.appId, context }),
3411
3477
  signal
3412
3478
  });
3413
- if (!res.ok || stopped) {
3479
+ if (stopped) {
3480
+ return;
3481
+ }
3482
+ if (!res.ok) {
3483
+ recordFailure();
3414
3484
  return;
3415
3485
  }
3416
3486
  const data = await res.json();
3417
3487
  if (!data.label) {
3488
+ recordFailure();
3418
3489
  return;
3419
3490
  }
3491
+ consecutiveFailures = 0;
3492
+ backoffMs = 0;
3493
+ nextAllowedAt = 0;
3420
3494
  if (pauseCount > 0 || stopped || signal?.aborted) {
3421
3495
  return;
3422
3496
  }
3423
3497
  onStatus(data.label);
3424
3498
  } catch {
3499
+ if (!stopped && !signal?.aborted) {
3500
+ recordFailure();
3501
+ }
3425
3502
  } finally {
3426
3503
  inflight2 = false;
3427
3504
  }
@@ -3518,7 +3595,12 @@ ${summaryBlock.text}
3518
3595
  }
3519
3596
  startIdx = checkpointIdx + 1;
3520
3597
  }
3521
- const messagesToProcess = fixOrphanedToolCalls(messages.slice(startIdx));
3598
+ const apiMessages = messages.slice(startIdx).filter(
3599
+ (msg) => !(msg.role === "assistant" && Array.isArray(msg.content) && msg.content.some(
3600
+ (b) => b.type === "tool" && b.uiOnly
3601
+ ))
3602
+ );
3603
+ const messagesToProcess = fixOrphanedToolCalls(apiMessages);
3522
3604
  const toolUseIds = /* @__PURE__ */ new Set();
3523
3605
  for (const msg of messagesToProcess) {
3524
3606
  if (msg.role === "assistant" && Array.isArray(msg.content)) {
@@ -3660,329 +3742,341 @@ ${partial}` : "[INTERRUPTED] Agent was interrupted before producing output.",
3660
3742
  return { text: USER_CANCELLED_RESULT, messages: thisInvocation() };
3661
3743
  }
3662
3744
  let lastToolResult = "";
3663
- while (true) {
3664
- turns++;
3665
- if (signal?.aborted) {
3666
- return abortResult([]);
3667
- }
3668
- const iterStart = Date.now();
3669
- const contentBlocks = [];
3670
- let thinkingStartedAt = 0;
3671
- let lastThinkingRelatedStartedAt;
3672
- let stopReason = "end_turn";
3673
- let currentToolNames = "";
3674
- let lastUsage;
3675
- let lastProviderMetadata;
3676
- let lastModelId;
3677
- const statusWatcher = startStatusWatcher({
3678
- apiConfig,
3679
- getContext: () => {
3680
- const parts = [];
3681
- if (task) {
3682
- parts.push(`Task: ${task.slice(-200)}`);
3683
- }
3684
- const text = getPartialText(contentBlocks);
3685
- if (text) {
3686
- parts.push(`Assistant text: ${text.slice(-500)}`);
3687
- }
3688
- if (currentToolNames) {
3689
- parts.push(`Tool: ${currentToolNames}`);
3690
- }
3691
- if (lastToolResult) {
3692
- parts.push(`Tool result: ${lastToolResult.slice(-200)}`);
3693
- }
3694
- return parts.join("\n");
3695
- },
3696
- onStatus: (label) => emit({ type: "status", message: label }),
3697
- signal
3698
- });
3699
- try {
3700
- for await (const event of streamChatWithRetry(
3701
- {
3702
- ...apiConfig,
3703
- model,
3704
- requestId,
3705
- subAgentId,
3706
- system: fullSystem,
3707
- messages: cleanMessagesForApi(messages),
3708
- tools: tools2,
3709
- signal
3710
- },
3711
- {
3712
- onRetry: (attempt) => emit({
3713
- type: "status",
3714
- message: `Lost connection, retrying (attempt ${attempt + 2} of 3)`
3715
- })
3745
+ let watchedBlocks = [];
3746
+ let watchedToolNames = "";
3747
+ const statusWatcher = startStatusWatcher({
3748
+ apiConfig,
3749
+ getContext: () => {
3750
+ const parts = [];
3751
+ if (task) {
3752
+ parts.push(`Task: ${sanitizeStatusText(task).slice(-200)}`);
3753
+ }
3754
+ const text = getPartialText(watchedBlocks);
3755
+ if (text) {
3756
+ parts.push(`Assistant text: ${text.slice(-500)}`);
3757
+ }
3758
+ if (watchedToolNames) {
3759
+ parts.push(`Tool: ${watchedToolNames}`);
3760
+ }
3761
+ const toolResult = sanitizeStatusText(lastToolResult);
3762
+ if (toolResult) {
3763
+ parts.push(`Tool result: ${toolResult.slice(-200)}`);
3764
+ }
3765
+ return parts.join("\n");
3766
+ },
3767
+ onStatus: (label) => emit({ type: "status", message: label }),
3768
+ signal
3769
+ });
3770
+ try {
3771
+ while (true) {
3772
+ turns++;
3773
+ if (signal?.aborted) {
3774
+ return abortResult([]);
3775
+ }
3776
+ const iterStart = Date.now();
3777
+ const contentBlocks = [];
3778
+ let thinkingStartedAt = 0;
3779
+ let lastThinkingRelatedStartedAt;
3780
+ let stopReason = "end_turn";
3781
+ let lastUsage;
3782
+ let lastProviderMetadata;
3783
+ let lastModelId;
3784
+ watchedBlocks = contentBlocks;
3785
+ watchedToolNames = "";
3786
+ try {
3787
+ for await (const event of streamChatWithRetry(
3788
+ {
3789
+ ...apiConfig,
3790
+ model,
3791
+ requestId,
3792
+ subAgentId,
3793
+ system: fullSystem,
3794
+ messages: cleanMessagesForApi(messages),
3795
+ tools: tools2,
3796
+ signal
3797
+ },
3798
+ {
3799
+ onRetry: (attempt) => emit({
3800
+ type: "status",
3801
+ message: `Lost connection, retrying (attempt ${attempt + 2} of 3)`
3802
+ })
3803
+ }
3804
+ )) {
3805
+ if (signal?.aborted) {
3806
+ break;
3807
+ }
3808
+ switch (event.type) {
3809
+ case "text": {
3810
+ const lastBlock = contentBlocks.at(-1);
3811
+ if (lastBlock?.type === "text") {
3812
+ lastBlock.text += event.text;
3813
+ } else {
3814
+ contentBlocks.push({
3815
+ type: "text",
3816
+ text: event.text,
3817
+ startedAt: event.ts
3818
+ });
3819
+ }
3820
+ emit({ type: "text", text: event.text });
3821
+ break;
3822
+ }
3823
+ case "thinking":
3824
+ if (!thinkingStartedAt) {
3825
+ thinkingStartedAt = event.ts;
3826
+ }
3827
+ emit({ type: "thinking", text: event.text });
3828
+ break;
3829
+ case "thinking_complete":
3830
+ contentBlocks.push({
3831
+ type: "thinking",
3832
+ thinking: event.thinking,
3833
+ signature: event.signature,
3834
+ startedAt: thinkingStartedAt,
3835
+ completedAt: event.ts
3836
+ });
3837
+ lastThinkingRelatedStartedAt = thinkingStartedAt;
3838
+ thinkingStartedAt = 0;
3839
+ break;
3840
+ case "redacted_thinking_complete": {
3841
+ const startedAt = lastThinkingRelatedStartedAt !== void 0 ? lastThinkingRelatedStartedAt + 1 : event.ts;
3842
+ contentBlocks.push({
3843
+ type: "redacted_thinking",
3844
+ data: event.data,
3845
+ startedAt,
3846
+ completedAt: event.ts
3847
+ });
3848
+ lastThinkingRelatedStartedAt = startedAt;
3849
+ break;
3850
+ }
3851
+ case "tool_use":
3852
+ contentBlocks.push({
3853
+ type: "tool",
3854
+ id: event.id,
3855
+ name: event.name,
3856
+ input: event.input,
3857
+ startedAt: Date.now()
3858
+ });
3859
+ emit({
3860
+ type: "tool_start",
3861
+ id: event.id,
3862
+ name: event.name,
3863
+ input: event.input
3864
+ });
3865
+ break;
3866
+ case "done":
3867
+ stopReason = event.stopReason;
3868
+ lastUsage = {
3869
+ inputTokens: event.usage.inputTokens,
3870
+ outputTokens: event.usage.outputTokens,
3871
+ cacheCreationTokens: event.usage.cacheCreationTokens,
3872
+ cacheReadTokens: event.usage.cacheReadTokens,
3873
+ llmCalls: 1
3874
+ };
3875
+ lastProviderMetadata = event.providerMetadata;
3876
+ lastModelId = event.modelId;
3877
+ recordUsage({
3878
+ ts: Date.now(),
3879
+ requestId,
3880
+ agentName: subAgentId || "sub-agent",
3881
+ parentToolId,
3882
+ modelId: event.modelId,
3883
+ inputTokens: event.usage.inputTokens,
3884
+ outputTokens: event.usage.outputTokens,
3885
+ cacheCreationTokens: event.usage.cacheCreationTokens,
3886
+ cacheReadTokens: event.usage.cacheReadTokens,
3887
+ cost: nanoToDollars(event.cost),
3888
+ billingEvents: event.billingEvents,
3889
+ durationMs: Date.now() - iterStart,
3890
+ toolNames: contentBlocks.filter(
3891
+ (b) => b.type === "tool"
3892
+ ).map((b) => b.name)
3893
+ });
3894
+ break;
3895
+ case "error":
3896
+ statusWatcher.stop();
3897
+ return {
3898
+ text: `Error: ${event.error}`,
3899
+ messages: thisInvocation()
3900
+ };
3901
+ }
3716
3902
  }
3717
- )) {
3718
- if (signal?.aborted) {
3719
- break;
3903
+ } catch (err) {
3904
+ if (!signal?.aborted) {
3905
+ throw err;
3720
3906
  }
3721
- switch (event.type) {
3722
- case "text": {
3723
- const lastBlock = contentBlocks.at(-1);
3724
- if (lastBlock?.type === "text") {
3725
- lastBlock.text += event.text;
3726
- } else {
3727
- contentBlocks.push({
3728
- type: "text",
3729
- text: event.text,
3730
- startedAt: event.ts
3907
+ }
3908
+ if (signal?.aborted) {
3909
+ statusWatcher.stop();
3910
+ return abortResult(contentBlocks);
3911
+ }
3912
+ messages.push({
3913
+ role: "assistant",
3914
+ content: contentBlocks,
3915
+ ...lastUsage ? { usage: lastUsage } : {},
3916
+ ...lastProviderMetadata ? { providerMetadata: lastProviderMetadata } : {},
3917
+ model: lastModelId ?? model
3918
+ });
3919
+ const toolCalls = contentBlocks.filter(
3920
+ (b) => b.type === "tool"
3921
+ );
3922
+ if (stopReason !== "tool_use" || toolCalls.length === 0) {
3923
+ statusWatcher.stop();
3924
+ const text = getPartialText(contentBlocks);
3925
+ const hasArtifacts = Object.keys(artifacts).length > 0;
3926
+ return {
3927
+ text,
3928
+ messages: thisInvocation(),
3929
+ ...hasArtifacts ? { artifacts } : {}
3930
+ };
3931
+ }
3932
+ log7.info("Tools executing", {
3933
+ requestId,
3934
+ parentToolId,
3935
+ count: toolCalls.length,
3936
+ tools: toolCalls.map((tc) => tc.name)
3937
+ });
3938
+ watchedToolNames = toolCalls.map((tc) => tc.name).join(", ");
3939
+ const results = await Promise.all(
3940
+ toolCalls.map(async (tc) => {
3941
+ if (signal?.aborted) {
3942
+ return {
3943
+ id: tc.id,
3944
+ result: USER_CANCELLED_RESULT,
3945
+ isError: true
3946
+ };
3947
+ }
3948
+ let settle;
3949
+ const resultPromise = new Promise((res) => {
3950
+ settle = (result, isError) => res({ id: tc.id, result, isError });
3951
+ });
3952
+ let toolAbort = new AbortController();
3953
+ const cascadeAbort = () => toolAbort.abort();
3954
+ signal?.addEventListener("abort", cascadeAbort, { once: true });
3955
+ let settled = false;
3956
+ const safeSettle = (result, isError) => {
3957
+ if (settled) {
3958
+ return;
3959
+ }
3960
+ settled = true;
3961
+ signal?.removeEventListener("abort", cascadeAbort);
3962
+ settle(result, isError);
3963
+ };
3964
+ const run2 = async (input) => {
3965
+ try {
3966
+ let result;
3967
+ if (externalTools.has(tc.name) && resolveExternalTool) {
3968
+ result = await resolveExternalTool(tc.id, tc.name, input);
3969
+ } else {
3970
+ const onLog = (line) => emit({
3971
+ type: "tool_input_delta",
3972
+ id: tc.id,
3973
+ name: tc.name,
3974
+ result: line
3975
+ });
3976
+ result = await executeTool2(
3977
+ tc.name,
3978
+ input,
3979
+ tc.id,
3980
+ onLog,
3981
+ subAgentMessages
3982
+ );
3983
+ }
3984
+ safeSettle(capToolResult(result), result.startsWith("Error"));
3985
+ } catch (err) {
3986
+ safeSettle(`Error: ${err.message}`, true);
3987
+ }
3988
+ };
3989
+ const entry = {
3990
+ id: tc.id,
3991
+ name: tc.name,
3992
+ input: tc.input,
3993
+ parentToolId,
3994
+ abortController: toolAbort,
3995
+ startedAt: Date.now(),
3996
+ settle: safeSettle,
3997
+ rerun: (newInput) => {
3998
+ settled = false;
3999
+ toolAbort = new AbortController();
4000
+ signal?.addEventListener("abort", () => toolAbort.abort(), {
4001
+ once: true
3731
4002
  });
4003
+ entry.abortController = toolAbort;
4004
+ entry.input = newInput;
4005
+ run2(newInput);
3732
4006
  }
3733
- emit({ type: "text", text: event.text });
3734
- break;
4007
+ };
4008
+ toolRegistry?.register(entry);
4009
+ const toolStart = Date.now();
4010
+ run2(tc.input);
4011
+ const r = await resultPromise;
4012
+ toolRegistry?.unregister(tc.id);
4013
+ log7.info("Tool completed", {
4014
+ requestId,
4015
+ parentToolId,
4016
+ toolCallId: tc.id,
4017
+ name: tc.name,
4018
+ durationMs: Date.now() - toolStart,
4019
+ isError: r.isError
4020
+ });
4021
+ emit({
4022
+ type: "tool_done",
4023
+ id: tc.id,
4024
+ name: tc.name,
4025
+ result: r.result,
4026
+ isError: r.isError
4027
+ });
4028
+ return r;
4029
+ })
4030
+ );
4031
+ lastToolResult = results.at(-1)?.result ?? "";
4032
+ for (const r of results) {
4033
+ const block = contentBlocks.find(
4034
+ (b) => b.type === "tool" && b.id === r.id
4035
+ );
4036
+ if (block?.type === "tool") {
4037
+ block.result = r.result;
4038
+ block.isError = r.isError;
4039
+ block.completedAt = Date.now();
4040
+ const innerMsgs = subAgentMessages.get(r.id);
4041
+ if (innerMsgs) {
4042
+ block.subAgentMessages = innerMsgs;
3735
4043
  }
3736
- case "thinking":
3737
- if (!thinkingStartedAt) {
3738
- thinkingStartedAt = event.ts;
4044
+ if (captureArtifacts?.includes(block.name) && !r.isError) {
4045
+ try {
4046
+ artifacts[block.name] = JSON.parse(r.result);
4047
+ } catch {
3739
4048
  }
3740
- emit({ type: "thinking", text: event.text });
3741
- break;
3742
- case "thinking_complete":
3743
- contentBlocks.push({
3744
- type: "thinking",
3745
- thinking: event.thinking,
3746
- signature: event.signature,
3747
- startedAt: thinkingStartedAt,
3748
- completedAt: event.ts
3749
- });
3750
- lastThinkingRelatedStartedAt = thinkingStartedAt;
3751
- thinkingStartedAt = 0;
3752
- break;
3753
- case "redacted_thinking_complete": {
3754
- const startedAt = lastThinkingRelatedStartedAt !== void 0 ? lastThinkingRelatedStartedAt + 1 : event.ts;
3755
- contentBlocks.push({
3756
- type: "redacted_thinking",
3757
- data: event.data,
3758
- startedAt,
3759
- completedAt: event.ts
3760
- });
3761
- lastThinkingRelatedStartedAt = startedAt;
3762
- break;
3763
4049
  }
3764
- case "tool_use":
3765
- contentBlocks.push({
3766
- type: "tool",
3767
- id: event.id,
3768
- name: event.name,
3769
- input: event.input,
3770
- startedAt: Date.now()
3771
- });
3772
- emit({
3773
- type: "tool_start",
3774
- id: event.id,
3775
- name: event.name,
3776
- input: event.input
3777
- });
3778
- break;
3779
- case "done":
3780
- stopReason = event.stopReason;
3781
- lastUsage = {
3782
- inputTokens: event.usage.inputTokens,
3783
- outputTokens: event.usage.outputTokens,
3784
- cacheCreationTokens: event.usage.cacheCreationTokens,
3785
- cacheReadTokens: event.usage.cacheReadTokens,
3786
- llmCalls: 1
3787
- };
3788
- lastProviderMetadata = event.providerMetadata;
3789
- lastModelId = event.modelId;
3790
- recordUsage({
3791
- ts: Date.now(),
3792
- requestId,
3793
- agentName: subAgentId || "sub-agent",
3794
- parentToolId,
3795
- modelId: event.modelId,
3796
- inputTokens: event.usage.inputTokens,
3797
- outputTokens: event.usage.outputTokens,
3798
- cacheCreationTokens: event.usage.cacheCreationTokens,
3799
- cacheReadTokens: event.usage.cacheReadTokens,
3800
- cost: nanoToDollars(event.cost),
3801
- billingEvents: event.billingEvents,
3802
- durationMs: Date.now() - iterStart,
3803
- toolNames: contentBlocks.filter(
3804
- (b) => b.type === "tool"
3805
- ).map((b) => b.name)
3806
- });
3807
- break;
3808
- case "error":
3809
- return {
3810
- text: `Error: ${event.error}`,
3811
- messages: thisInvocation()
3812
- };
3813
4050
  }
3814
- }
3815
- } catch (err) {
3816
- if (!signal?.aborted) {
3817
- throw err;
4051
+ messages.push({
4052
+ role: "user",
4053
+ content: r.result,
4054
+ toolCallId: r.id,
4055
+ isToolError: r.isError
4056
+ });
3818
4057
  }
3819
4058
  }
3820
- if (signal?.aborted) {
3821
- statusWatcher.stop();
3822
- return abortResult(contentBlocks);
3823
- }
3824
- messages.push({
3825
- role: "assistant",
3826
- content: contentBlocks,
3827
- ...lastUsage ? { usage: lastUsage } : {},
3828
- ...lastProviderMetadata ? { providerMetadata: lastProviderMetadata } : {},
3829
- model: lastModelId ?? model
4059
+ } finally {
4060
+ statusWatcher.stop();
4061
+ }
4062
+ };
4063
+ const wrapRun = async () => {
4064
+ try {
4065
+ const result = await run();
4066
+ log7.info("Sub-agent complete", {
4067
+ requestId,
4068
+ parentToolId,
4069
+ agentName,
4070
+ durationMs: Date.now() - runStart,
4071
+ turns
3830
4072
  });
3831
- const toolCalls = contentBlocks.filter(
3832
- (b) => b.type === "tool"
3833
- );
3834
- if (stopReason !== "tool_use" || toolCalls.length === 0) {
3835
- statusWatcher.stop();
3836
- const text = getPartialText(contentBlocks);
3837
- const hasArtifacts = Object.keys(artifacts).length > 0;
3838
- return {
3839
- text,
3840
- messages: thisInvocation(),
3841
- ...hasArtifacts ? { artifacts } : {}
3842
- };
3843
- }
3844
- log7.info("Tools executing", {
4073
+ return result;
4074
+ } catch (err) {
4075
+ log7.warn("Sub-agent error", {
3845
4076
  requestId,
3846
4077
  parentToolId,
3847
- count: toolCalls.length,
3848
- tools: toolCalls.map((tc) => tc.name)
3849
- });
3850
- currentToolNames = toolCalls.map((tc) => tc.name).join(", ");
3851
- const results = await Promise.all(
3852
- toolCalls.map(async (tc) => {
3853
- if (signal?.aborted) {
3854
- return { id: tc.id, result: USER_CANCELLED_RESULT, isError: true };
3855
- }
3856
- let settle;
3857
- const resultPromise = new Promise((res) => {
3858
- settle = (result, isError) => res({ id: tc.id, result, isError });
3859
- });
3860
- let toolAbort = new AbortController();
3861
- const cascadeAbort = () => toolAbort.abort();
3862
- signal?.addEventListener("abort", cascadeAbort, { once: true });
3863
- let settled = false;
3864
- const safeSettle = (result, isError) => {
3865
- if (settled) {
3866
- return;
3867
- }
3868
- settled = true;
3869
- signal?.removeEventListener("abort", cascadeAbort);
3870
- settle(result, isError);
3871
- };
3872
- const run2 = async (input) => {
3873
- try {
3874
- let result;
3875
- if (externalTools.has(tc.name) && resolveExternalTool) {
3876
- result = await resolveExternalTool(tc.id, tc.name, input);
3877
- } else {
3878
- const onLog = (line) => emit({
3879
- type: "tool_input_delta",
3880
- id: tc.id,
3881
- name: tc.name,
3882
- result: line
3883
- });
3884
- result = await executeTool2(
3885
- tc.name,
3886
- input,
3887
- tc.id,
3888
- onLog,
3889
- subAgentMessages
3890
- );
3891
- }
3892
- safeSettle(capToolResult(result), result.startsWith("Error"));
3893
- } catch (err) {
3894
- safeSettle(`Error: ${err.message}`, true);
3895
- }
3896
- };
3897
- const entry = {
3898
- id: tc.id,
3899
- name: tc.name,
3900
- input: tc.input,
3901
- parentToolId,
3902
- abortController: toolAbort,
3903
- startedAt: Date.now(),
3904
- settle: safeSettle,
3905
- rerun: (newInput) => {
3906
- settled = false;
3907
- toolAbort = new AbortController();
3908
- signal?.addEventListener("abort", () => toolAbort.abort(), {
3909
- once: true
3910
- });
3911
- entry.abortController = toolAbort;
3912
- entry.input = newInput;
3913
- run2(newInput);
3914
- }
3915
- };
3916
- toolRegistry?.register(entry);
3917
- const toolStart = Date.now();
3918
- run2(tc.input);
3919
- const r = await resultPromise;
3920
- toolRegistry?.unregister(tc.id);
3921
- log7.info("Tool completed", {
3922
- requestId,
3923
- parentToolId,
3924
- toolCallId: tc.id,
3925
- name: tc.name,
3926
- durationMs: Date.now() - toolStart,
3927
- isError: r.isError
3928
- });
3929
- emit({
3930
- type: "tool_done",
3931
- id: tc.id,
3932
- name: tc.name,
3933
- result: r.result,
3934
- isError: r.isError
3935
- });
3936
- return r;
3937
- })
3938
- );
3939
- statusWatcher.stop();
3940
- lastToolResult = results.at(-1)?.result ?? "";
3941
- for (const r of results) {
3942
- const block = contentBlocks.find(
3943
- (b) => b.type === "tool" && b.id === r.id
3944
- );
3945
- if (block?.type === "tool") {
3946
- block.result = r.result;
3947
- block.isError = r.isError;
3948
- block.completedAt = Date.now();
3949
- const innerMsgs = subAgentMessages.get(r.id);
3950
- if (innerMsgs) {
3951
- block.subAgentMessages = innerMsgs;
3952
- }
3953
- if (captureArtifacts?.includes(block.name) && !r.isError) {
3954
- try {
3955
- artifacts[block.name] = JSON.parse(r.result);
3956
- } catch {
3957
- }
3958
- }
3959
- }
3960
- messages.push({
3961
- role: "user",
3962
- content: r.result,
3963
- toolCallId: r.id,
3964
- isToolError: r.isError
3965
- });
3966
- }
3967
- }
3968
- };
3969
- const wrapRun = async () => {
3970
- try {
3971
- const result = await run();
3972
- log7.info("Sub-agent complete", {
3973
- requestId,
3974
- parentToolId,
3975
- agentName,
3976
- durationMs: Date.now() - runStart,
3977
- turns
3978
- });
3979
- return result;
3980
- } catch (err) {
3981
- log7.warn("Sub-agent error", {
3982
- requestId,
3983
- parentToolId,
3984
- agentName,
3985
- error: err.message
4078
+ agentName,
4079
+ error: err.message
3986
4080
  });
3987
4081
  throw err;
3988
4082
  }
@@ -5929,6 +6023,122 @@ var codeSanityCheckTool = {
5929
6023
  }
5930
6024
  };
5931
6025
 
6026
+ // src/tools/spec/writeBuildOverview.ts
6027
+ import fs20 from "fs";
6028
+ var OVERVIEW_FILE = "src/overview.html";
6029
+ var DESIGN_BRIEF = `We are building the Build Overview for this app \u2014 the home page of its Spec tab. It is a calm, dense, one-page reference of everything the app actually contains, including the parts the user can't see. It renders flush inside the Spec tab's content panel (the IDE supplies the surrounding nav).
6030
+
6031
+ Take the plain-language copy in <overview_copy> and lay it out and skin it into a single, beautiful, self-contained HTML document in the app's own brand.
6032
+
6033
+ ### The single hard rule
6034
+ The copy in <overview_copy> is final \u2014 it was authored and edited before it reached you. Treat it as locked content to typeset, not a draft to improve. Reproduce the words exactly: do not rewrite, rephrase, shorten, expand, reorder, or "polish" them, and do not run them through any copy tool. This is the opposite of your usual role \u2014 here you own layout, typography, and visual design only, and the words (every number, name, label, claim, and sentence) are fixed. A single changed word or wrong number breaks this document's purpose.
6035
+
6036
+ ### What it is (and is not)
6037
+ - A typeset reference dossier: composed, dense, a little cool \u2014 substantial at a glance, then readable. Density communicates substance; sparse and airy reads as "not much here."
6038
+ - Brand-skinned: pull the palette, type system, and a single accent from the app's spec. The accent leads labels and figures; it is a mark, not a fill.
6039
+ - NOT a slide deck \u2014 no theatrics, no building to a climax, no persuasion.
6040
+ - NOT a docs site \u2014 no raised or shadowed cards, no hover-lift, no clickable-feeling surfaces, no sticky table of contents.
6041
+ - NOT a sparse memo \u2014 a restrained single-column page under-sells the work.
6042
+ - No header chrome and no footer: open straight on the app's logo and name so it sits flush with the IDE.
6043
+
6044
+ ### Constraints
6045
+ - A single self-contained HTML file. Fonts may load from a CDN; everything else (CSS, the logo SVG) is inline.
6046
+ - Responsive: fills the embedded panel width and collapses gracefully at narrow widths.`;
6047
+ var OVERVIEW_SHELL = `<!DOCTYPE html>
6048
+ <html lang="en">
6049
+ <head>
6050
+ <meta charset="utf-8">
6051
+ <!-- This renders inside an iframe in the IDE. Keep it a simple, self-contained
6052
+ static document \u2014 no scroll/zoom/accessibility scaffolding beyond this head.
6053
+ The user-scalable=no viewport is intentional; leave it. -->
6054
+ <meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
6055
+ <title>Build Overview</title>
6056
+ <!-- SKIN: link the app's fonts here (CDN \u2014 e.g. Fontshare / Google Fonts). -->
6057
+ <style>
6058
+ *, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
6059
+ </style>
6060
+ </head>
6061
+ <body>
6062
+ <!-- Compose the Build Overview here. Layout, sections, type, and styling are
6063
+ yours \u2014 keep everything inline and self-contained, brand-skinned from the
6064
+ app's spec. -->
6065
+ </body>
6066
+ </html>`;
6067
+ function initialDelivery() {
6068
+ return `### Your deliverable
6069
+ Write the complete Build Overview to \`${OVERVIEW_FILE}\`. The file does not exist yet \u2014 start from this scaffold, which carries the technical hygiene for the iframed render context (keep the viewport meta and the reset). Layout, sections, type, and styling are yours to compose.
6070
+
6071
+ <overview_shell>
6072
+ ${OVERVIEW_SHELL}
6073
+ </overview_shell>
6074
+
6075
+ Then reply with a one-line summary of what you wrote.`;
6076
+ }
6077
+ function refreshDelivery() {
6078
+ return `### Your deliverable
6079
+ The Build Overview already exists at \`${OVERVIEW_FILE}\`. Read it, then update it to reflect <overview_copy>, preserving its established skin \u2014 change only what the copy changed.
6080
+
6081
+ Then reply with a one-line summary of what you changed.`;
6082
+ }
6083
+ async function renderBuildOverview(content, context, opts) {
6084
+ if (!content) {
6085
+ return "Error: writeBuildOverview requires non-empty `content` (the overview copy).";
6086
+ }
6087
+ const exists = fs20.existsSync(OVERVIEW_FILE);
6088
+ const task = `<overview_copy>${content}</overview_copy>
6089
+
6090
+ ${DESIGN_BRIEF}
6091
+
6092
+ ${exists ? refreshDelivery() : initialDelivery()}`;
6093
+ try {
6094
+ if (opts.background) {
6095
+ const result2 = await runDesignExpertRender(
6096
+ { task, background: true, reportingName: "writeBuildOverview" },
6097
+ context
6098
+ );
6099
+ context.subAgentMessages?.set(context.toolCallId, result2.messages);
6100
+ return result2.text;
6101
+ }
6102
+ const result = await runDesignExpertRender({ task }, context);
6103
+ context.subAgentMessages?.set(context.toolCallId, result.messages);
6104
+ if (!fs20.existsSync(OVERVIEW_FILE)) {
6105
+ return `Error: the design expert did not write ${OVERVIEW_FILE}. Its reply was:
6106
+ ${result.text}`;
6107
+ }
6108
+ return `Build overview written to ${OVERVIEW_FILE}. ${result.text}`;
6109
+ } catch (err) {
6110
+ return `Error generating build overview: ${err.message}`;
6111
+ }
6112
+ }
6113
+ var buildOverviewTool = {
6114
+ definition: {
6115
+ clearable: false,
6116
+ name: "writeBuildOverview",
6117
+ description: "Generate or refresh the Build Overview \u2014 the project's home page in the Spec tab: a single-page, plain-language reference of everything the app actually contains, including the parts the user can't see (data stores, backend operations, access and roles, background jobs, seeded scenarios, the design system). You author the full copy: read the manifest and spec and state, plainly and exactly, what genuinely exists \u2014 real names and accurate counts \u2014 in calm, declarative, present-tense outcome language, with no persuasion or hype. Describe only what exists. Pass the complete copy as `content`; the design expert lays it out and skins it to the app's brand using your copy verbatim \u2014 it typesets your words, it does not rewrite them, so polish the copy before you pass it. Generate it at the end of a build; routine refreshes ride specSync's `refreshBuildOverview` flag instead, so call this directly only for the initial generation or when the user explicitly asks.",
6118
+ inputSchema: {
6119
+ type: "object",
6120
+ properties: {
6121
+ content: {
6122
+ type: "string",
6123
+ description: "The full Build Overview copy you authored: everything the app contains, in plain present-tense outcome language, with real names and exact counts. The design expert lays this out and skins it to the brand verbatim \u2014 it styles your words, it does not rewrite them \u2014 so this should be the final copy."
6124
+ }
6125
+ },
6126
+ required: ["content"]
6127
+ }
6128
+ },
6129
+ async execute(input, context) {
6130
+ if (!context) {
6131
+ return "Error: writeBuildOverview requires execution context for design expert delegation";
6132
+ }
6133
+ const content = (input.content ?? "").trim();
6134
+ const background = Boolean(content) && fs20.existsSync(OVERVIEW_FILE);
6135
+ if (background) {
6136
+ input.background = true;
6137
+ }
6138
+ return renderBuildOverview(content, context, { background });
6139
+ }
6140
+ };
6141
+
5932
6142
  // src/subagents/specSync/tools.ts
5933
6143
  var SPEC_SYNC_TOOLS = [
5934
6144
  ...COMMON_READ_TOOLS,
@@ -5965,13 +6175,17 @@ var specSyncTool = {
5965
6175
  definition: {
5966
6176
  clearable: false,
5967
6177
  name: "specSync",
5968
- description: "Reconcile the spec to bring it in line with code changes you have made. Provide a brief, bulleted list of what changed and why; it finds the affected spec sections and updates them to match. Always runs in the background and completes silently \u2014 do not wait for it; its outcome appears as an automated note at the start of a later turn.",
6178
+ description: "Reconcile the spec to bring it in line with code changes you have made. Provide a brief, bulleted list of what changed and why; it finds the affected spec sections and updates them to match. Set `refreshBuildOverview` after a deploy or a large milestone to also re-author the Build Overview from the updated spec. Always runs in the background and completes silently \u2014 do not wait for it; its outcome appears as an automated note at the start of a later turn.",
5969
6179
  inputSchema: {
5970
6180
  type: "object",
5971
6181
  properties: {
5972
6182
  task: {
5973
6183
  type: "string",
5974
6184
  description: "What you changed in the code and why, in plain language \u2014 the way you would explain it to a teammate. The specialist reads the spec itself and decides which sections to update; you do not need to name files or spec locations."
6185
+ },
6186
+ refreshBuildOverview: {
6187
+ type: "boolean",
6188
+ description: "Also refresh the Build Overview (src/overview.html) after reconciling \u2014 the specialist authors fresh copy from the updated spec and re-renders the page. Set this after a deploy or a large milestone; leave it off for routine syncs."
5975
6189
  }
5976
6190
  },
5977
6191
  required: ["task"]
@@ -5995,12 +6209,27 @@ var specSyncTool = {
5995
6209
  parts.push(specIndex);
5996
6210
  }
5997
6211
  const system = parts.join("\n\n");
6212
+ const refreshOverview = input.refreshBuildOverview === true;
6213
+ const tools2 = refreshOverview ? [...SPEC_SYNC_TOOLS, buildOverviewTool.definition] : SPEC_SYNC_TOOLS;
6214
+ const task = refreshOverview ? `${input.task}
6215
+
6216
+ After reconciling the spec, refresh the Build Overview: author the complete updated copy from the freshly-reconciled spec (follow the writeBuildOverview tool description) and call \`writeBuildOverview\` once with the final copy.` : input.task;
5998
6217
  const result = await runSubAgent({
5999
6218
  system,
6000
- task: input.task,
6001
- tools: SPEC_SYNC_TOOLS,
6219
+ task,
6220
+ tools: tools2,
6002
6221
  externalTools: /* @__PURE__ */ new Set(),
6003
- executeTool: (name, toolInput) => executeTool(name, toolInput, context),
6222
+ executeTool: (name, toolInput, toolCallId, _onLog, sams) => {
6223
+ if (name === "writeBuildOverview") {
6224
+ const childCtx = toolCallId ? { ...deriveContext(context, toolCallId), subAgentMessages: sams } : { ...context, subAgentMessages: sams };
6225
+ return renderBuildOverview(
6226
+ String(toolInput.content ?? "").trim(),
6227
+ childCtx,
6228
+ { background: false }
6229
+ );
6230
+ }
6231
+ return executeTool(name, toolInput, context);
6232
+ },
6004
6233
  apiConfig: context.apiConfig,
6005
6234
  model: resolveModel("specSync", context.models, context.model),
6006
6235
  subAgentId: "specSync",
@@ -6073,119 +6302,9 @@ var scrapeWebUrlTool = {
6073
6302
  }
6074
6303
  };
6075
6304
 
6076
- // src/tools/spec/writeBuildOverview.ts
6077
- import fs20 from "fs";
6078
- var OVERVIEW_FILE = "src/overview.html";
6079
- var DESIGN_BRIEF = `We are building the Build Overview for this app \u2014 the home page of its Spec tab. It is a calm, dense, one-page reference of everything the app actually contains, including the parts the user can't see. It renders flush inside the Spec tab's content panel (the IDE supplies the surrounding nav).
6080
-
6081
- Take the plain-language copy in <overview_copy> and lay it out and skin it into a single, beautiful, self-contained HTML document in the app's own brand.
6082
-
6083
- ### The single hard rule
6084
- The copy in <overview_copy> is final \u2014 it was authored and edited before it reached you. Treat it as locked content to typeset, not a draft to improve. Reproduce the words exactly: do not rewrite, rephrase, shorten, expand, reorder, or "polish" them, and do not run them through any copy tool. This is the opposite of your usual role \u2014 here you own layout, typography, and visual design only, and the words (every number, name, label, claim, and sentence) are fixed. A single changed word or wrong number breaks this document's purpose.
6085
-
6086
- ### What it is (and is not)
6087
- - A typeset reference dossier: composed, dense, a little cool \u2014 substantial at a glance, then readable. Density communicates substance; sparse and airy reads as "not much here."
6088
- - Brand-skinned: pull the palette, type system, and a single accent from the app's spec. The accent leads labels and figures; it is a mark, not a fill.
6089
- - NOT a slide deck \u2014 no theatrics, no building to a climax, no persuasion.
6090
- - NOT a docs site \u2014 no raised or shadowed cards, no hover-lift, no clickable-feeling surfaces, no sticky table of contents.
6091
- - NOT a sparse memo \u2014 a restrained single-column page under-sells the work.
6092
- - No header chrome and no footer: open straight on the app's logo and name so it sits flush with the IDE.
6093
-
6094
- ### Constraints
6095
- - A single self-contained HTML file. Fonts may load from a CDN; everything else (CSS, the logo SVG) is inline.
6096
- - Responsive: fills the embedded panel width and collapses gracefully at narrow widths.`;
6097
- var OVERVIEW_SHELL = `<!DOCTYPE html>
6098
- <html lang="en">
6099
- <head>
6100
- <meta charset="utf-8">
6101
- <!-- This renders inside an iframe in the IDE. Keep it a simple, self-contained
6102
- static document \u2014 no scroll/zoom/accessibility scaffolding beyond this head.
6103
- The user-scalable=no viewport is intentional; leave it. -->
6104
- <meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
6105
- <title>Build Overview</title>
6106
- <!-- SKIN: link the app's fonts here (CDN \u2014 e.g. Fontshare / Google Fonts). -->
6107
- <style>
6108
- *, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
6109
- </style>
6110
- </head>
6111
- <body>
6112
- <!-- Compose the Build Overview here. Layout, sections, type, and styling are
6113
- yours \u2014 keep everything inline and self-contained, brand-skinned from the
6114
- app's spec. -->
6115
- </body>
6116
- </html>`;
6117
- function initialDelivery() {
6118
- return `### Your deliverable
6119
- Write the complete Build Overview to \`${OVERVIEW_FILE}\`. The file does not exist yet \u2014 start from this scaffold, which carries the technical hygiene for the iframed render context (keep the viewport meta and the reset). Layout, sections, type, and styling are yours to compose.
6120
-
6121
- <overview_shell>
6122
- ${OVERVIEW_SHELL}
6123
- </overview_shell>
6124
-
6125
- Then reply with a one-line summary of what you wrote.`;
6126
- }
6127
- function refreshDelivery() {
6128
- return `### Your deliverable
6129
- The Build Overview already exists at \`${OVERVIEW_FILE}\`. Read it, then update it to reflect <overview_copy>, preserving its established skin \u2014 change only what the copy changed.
6130
-
6131
- Then reply with a one-line summary of what you changed.`;
6132
- }
6133
- var buildOverviewTool = {
6134
- definition: {
6135
- clearable: false,
6136
- name: "writeBuildOverview",
6137
- description: "Generate or refresh the Build Overview \u2014 the project's home page in the Spec tab: a single-page, plain-language reference of everything the app actually contains, including the parts the user can't see (data stores, backend operations, access and roles, background jobs, seeded scenarios, the design system). You author the full copy: read the manifest and spec and state, plainly and exactly, what genuinely exists \u2014 real names and accurate counts \u2014 in calm, declarative, present-tense outcome language, with no persuasion or hype. Describe only what exists. Pass the complete copy as `content`; the design expert lays it out and skins it to the app's brand using your copy verbatim \u2014 it typesets your words, it does not rewrite them, so polish the copy before you pass it. Generate it at the end of a build and refresh it after meaningful work.",
6138
- inputSchema: {
6139
- type: "object",
6140
- properties: {
6141
- content: {
6142
- type: "string",
6143
- description: "The full Build Overview copy you authored: everything the app contains, in plain present-tense outcome language, with real names and exact counts. The design expert lays this out and skins it to the brand verbatim \u2014 it styles your words, it does not rewrite them \u2014 so this should be the final copy."
6144
- }
6145
- },
6146
- required: ["content"]
6147
- }
6148
- },
6149
- async execute(input, context) {
6150
- if (!context) {
6151
- return "Error: writeBuildOverview requires execution context for design expert delegation";
6152
- }
6153
- const content = (input.content ?? "").trim();
6154
- if (!content) {
6155
- return "Error: writeBuildOverview requires non-empty `content` (the overview copy).";
6156
- }
6157
- const exists = fs20.existsSync(OVERVIEW_FILE);
6158
- const task = `<overview_copy>${content}</overview_copy>
6159
-
6160
- ${DESIGN_BRIEF}
6161
-
6162
- ${exists ? refreshDelivery() : initialDelivery()}`;
6163
- try {
6164
- if (exists) {
6165
- input.background = true;
6166
- const result2 = await runDesignExpertRender(
6167
- { task, background: true, reportingName: "writeBuildOverview" },
6168
- context
6169
- );
6170
- context.subAgentMessages?.set(context.toolCallId, result2.messages);
6171
- return result2.text;
6172
- }
6173
- const result = await runDesignExpertRender({ task }, context);
6174
- context.subAgentMessages?.set(context.toolCallId, result.messages);
6175
- if (!fs20.existsSync(OVERVIEW_FILE)) {
6176
- return `Error: the design expert did not write ${OVERVIEW_FILE}. Its reply was:
6177
- ${result.text}`;
6178
- }
6179
- return `Build overview written to ${OVERVIEW_FILE}. ${result.text}`;
6180
- } catch (err) {
6181
- return `Error generating build overview: ${err.message}`;
6182
- }
6183
- }
6184
- };
6185
-
6186
- // src/tools/index.ts
6187
- function deriveContext(parent, toolCallId) {
6188
- return { ...parent, toolCallId };
6305
+ // src/tools/index.ts
6306
+ function deriveContext(parent, toolCallId) {
6307
+ return { ...parent, toolCallId };
6189
6308
  }
6190
6309
  var ALL_TOOLS = [
6191
6310
  // Common
@@ -6381,7 +6500,11 @@ function getConversationMessagesForSummary(messages, endIndex) {
6381
6500
  break;
6382
6501
  }
6383
6502
  }
6384
- return messages.slice(startIdx, endIndex);
6503
+ return messages.slice(startIdx, endIndex).filter(
6504
+ (msg) => !(Array.isArray(msg.content) && msg.content.some(
6505
+ (b) => b.type === "tool" && b.uiOnly
6506
+ ))
6507
+ );
6385
6508
  }
6386
6509
  function getSubAgentMessagesForSummary(messages, subAgentName, endIndex) {
6387
6510
  let checkpointIdx = -1;
@@ -6659,6 +6782,11 @@ import path12 from "path";
6659
6782
  var log10 = createLogger("session");
6660
6783
  var SESSION_FILE = ".remy-session.json";
6661
6784
  var ARCHIVE_DIR = ".logs/sessions";
6785
+ function writeFileAtomicSync(file, data) {
6786
+ const tmp = `${file}.tmp`;
6787
+ fs21.writeFileSync(tmp, data, "utf-8");
6788
+ fs21.renameSync(tmp, file);
6789
+ }
6662
6790
  var ROTATE_THRESHOLD_BYTES = 32 * 1024 * 1024;
6663
6791
  var RETAIN_TAIL_BYTES = 16 * 1024 * 1024;
6664
6792
  var ARCHIVE_RETENTION_BYTES = 64 * 1024 * 1024;
@@ -6758,7 +6886,7 @@ function archiveMessages(messages, label, models) {
6758
6886
  if (models && Object.keys(models).length > 0) {
6759
6887
  payload.models = models;
6760
6888
  }
6761
- fs21.writeFileSync(dest, JSON.stringify(payload), "utf-8");
6889
+ writeFileAtomicSync(dest, JSON.stringify(payload));
6762
6890
  archiveCountCache.set(path12.basename(dest), count);
6763
6891
  log10.info("Session archived", { label, dest, messageCount: count });
6764
6892
  pruneArchives();
@@ -6963,7 +7091,7 @@ function saveSession(state) {
6963
7091
  if (Buffer.byteLength(serialized, "utf-8") > ROTATE_THRESHOLD_BYTES && rotate(state)) {
6964
7092
  serialized = JSON.stringify(buildPayload(state));
6965
7093
  }
6966
- fs21.writeFileSync(SESSION_FILE, serialized, "utf-8");
7094
+ writeFileAtomicSync(SESSION_FILE, serialized);
6967
7095
  log10.info("Session saved", { messageCount: state.messages.length });
6968
7096
  } catch (err) {
6969
7097
  log10.warn("Session save failed", { error: err.message });
@@ -6993,6 +7121,28 @@ function clearSession(state) {
6993
7121
  var log11 = createLogger("compaction:trigger");
6994
7122
  var pending = null;
6995
7123
  var inflightCompaction = null;
7124
+ function getInflightCompaction() {
7125
+ return inflightCompaction;
7126
+ }
7127
+ function summariesOf(result) {
7128
+ const out = [];
7129
+ for (const msg of result.checkpoints) {
7130
+ if (!Array.isArray(msg.content)) {
7131
+ continue;
7132
+ }
7133
+ for (const block of msg.content) {
7134
+ if (block.type === "summary") {
7135
+ out.push({
7136
+ name: block.name,
7137
+ text: block.text,
7138
+ ...block.recent && { recent: block.recent },
7139
+ startedAt: block.startedAt
7140
+ });
7141
+ }
7142
+ }
7143
+ }
7144
+ return out;
7145
+ }
6996
7146
  function applyPendingSummaries(state) {
6997
7147
  const drained = pending;
6998
7148
  pending = null;
@@ -7013,6 +7163,25 @@ function applyPendingSummaries(state) {
7013
7163
  });
7014
7164
  saveSession(state);
7015
7165
  }
7166
+ var SUMMARY_SECTION_LABELS = {
7167
+ conversation: "Conversation",
7168
+ visualDesignExpert: "Design Agent Thread",
7169
+ productVision: "Roadmap Agent Thread"
7170
+ };
7171
+ function formatSummariesResult(summaries) {
7172
+ if (summaries === null) {
7173
+ return "A compaction checkpoint was already pending \u2014 no new compaction was needed.";
7174
+ }
7175
+ if (summaries.length === 0) {
7176
+ return "Nothing to compact \u2014 the conversation is already fully summarized.";
7177
+ }
7178
+ if (summaries.length === 1 && summaries[0].name === "conversation") {
7179
+ return summaries[0].text;
7180
+ }
7181
+ return summaries.map((s) => `## ${SUMMARY_SECTION_LABELS[s.name] ?? s.name}
7182
+
7183
+ ${s.text}`).join("\n\n");
7184
+ }
7016
7185
  var listener = null;
7017
7186
  function setCompactionListener(l) {
7018
7187
  listener = l;
@@ -7023,18 +7192,24 @@ function triggerCompaction(state, apiConfig, opts = {}) {
7023
7192
  }
7024
7193
  if (pending) {
7025
7194
  log11.info("Compaction skipped \u2014 a checkpoint is already waiting to apply");
7026
- return Promise.resolve();
7195
+ return Promise.resolve(null);
7027
7196
  }
7028
- const { blocking = false, requestId, model } = opts;
7029
- listener?.({ type: "started", blocking, requestId });
7197
+ const { blocking = false, requestId, model, origin, toolCallId } = opts;
7198
+ listener?.({ type: "started", blocking, requestId, origin, toolCallId });
7030
7199
  inflightCompaction = compactConversation(
7031
7200
  state.messages,
7032
7201
  apiConfig,
7033
7202
  resolveModel("conversationSummarizer", state.models, model)
7034
7203
  ).then((result) => {
7035
7204
  pending = result;
7036
- listener?.({ type: "complete", requestId });
7205
+ const summaries = summariesOf(result);
7206
+ listener?.({
7207
+ type: "complete",
7208
+ requestId,
7209
+ ...summaries.length > 0 && { summaries }
7210
+ });
7037
7211
  log11.info("Compaction complete");
7212
+ return summaries;
7038
7213
  }).catch((err) => {
7039
7214
  const message = err.message || "Compaction failed";
7040
7215
  listener?.({ type: "complete", error: message, requestId });
@@ -7554,6 +7729,62 @@ function parsePartialJson(jsonString) {
7554
7729
  return parseAny();
7555
7730
  }
7556
7731
 
7732
+ // src/automatedActions/resolve.ts
7733
+ var NON_ACTION_SENTINELS = /* @__PURE__ */ new Set(["background_results"]);
7734
+ function resolveAction(text) {
7735
+ const parsed = parseSentinel(text);
7736
+ if (!parsed) {
7737
+ return null;
7738
+ }
7739
+ const { name: triggerName, remainder } = parsed;
7740
+ if (NON_ACTION_SENTINELS.has(triggerName)) {
7741
+ return null;
7742
+ }
7743
+ let params = {};
7744
+ if (remainder) {
7745
+ try {
7746
+ params = JSON.parse(remainder.split("\n")[0]);
7747
+ } catch {
7748
+ }
7749
+ }
7750
+ let body = readAsset("automatedActions", `${triggerName}.md`);
7751
+ let next;
7752
+ const fmMatch = body.match(/^---\s*\n([\s\S]*?)\n---/);
7753
+ if (fmMatch) {
7754
+ const nextMatch = fmMatch[1].match(/^\s*next:\s*(\w+)\s*$/m);
7755
+ if (nextMatch) {
7756
+ next = nextMatch[1];
7757
+ }
7758
+ }
7759
+ body = body.replace(/^---[\s\S]*?---\s*/, "");
7760
+ for (const [key, value] of Object.entries(params)) {
7761
+ const str = typeof value === "string" ? value : JSON.stringify(value);
7762
+ body = body.replaceAll(`{{${key}}}`, str);
7763
+ }
7764
+ return {
7765
+ message: automatedMessage(triggerName, body),
7766
+ next
7767
+ };
7768
+ }
7769
+ function getActionChain(startName) {
7770
+ const chain = [];
7771
+ const seen = /* @__PURE__ */ new Set();
7772
+ let name = startName;
7773
+ while (name && !seen.has(name)) {
7774
+ seen.add(name);
7775
+ chain.push(name);
7776
+ let body;
7777
+ try {
7778
+ body = readAsset("automatedActions", `${name}.md`);
7779
+ } catch {
7780
+ break;
7781
+ }
7782
+ const fm = body.match(/^---\s*\n([\s\S]*?)\n---/);
7783
+ name = fm?.[1].match(/^\s*next:\s*(\w+)\s*$/m)?.[1];
7784
+ }
7785
+ return chain;
7786
+ }
7787
+
7557
7788
  // src/errors.ts
7558
7789
  var patterns = [
7559
7790
  [
@@ -7705,6 +7936,9 @@ async function runTurn(params) {
7705
7936
  let lastCompletedTools = "";
7706
7937
  let lastCompletedInput = "";
7707
7938
  let lastCompletedResult = "";
7939
+ let subAgentText = "";
7940
+ let currentToolNames = "";
7941
+ let contentBlocksRef = [];
7708
7942
  let turnInputTokens = 0;
7709
7943
  let turnOutputTokens = 0;
7710
7944
  let turnCacheCreation = 0;
@@ -7713,281 +7947,328 @@ async function runTurn(params) {
7713
7947
  let lastCallInputTokens = 0;
7714
7948
  let lastCallCacheCreation = 0;
7715
7949
  let lastCallCacheRead = 0;
7716
- while (true) {
7717
- let getOrCreateAccumulator2 = function(id, name) {
7718
- let acc = toolInputAccumulators.get(id);
7719
- if (!acc) {
7720
- acc = { name, json: "", started: false, lastEmittedCount: 0 };
7721
- toolInputAccumulators.set(id, acc);
7722
- }
7723
- return acc;
7724
- };
7725
- var getOrCreateAccumulator = getOrCreateAccumulator2;
7726
- if (signal?.aborted) {
7727
- onEvent({ type: "turn_cancelled" });
7728
- saveSession(state);
7729
- return;
7730
- }
7731
- const iterStart = Date.now();
7732
- const contentBlocks = [];
7733
- const thinkingBlockStartTimes = [];
7734
- let thinkingCompleteCount = 0;
7735
- let lastThinkingRelatedStartedAt;
7736
- let textBlockOpen = false;
7737
- const toolInputAccumulators = /* @__PURE__ */ new Map();
7738
- let stopReason = "end_turn";
7739
- let turnProviderMetadata;
7740
- let turnModelId;
7741
- let subAgentText = "";
7742
- let currentToolNames = "";
7743
- const statusWatcher = isFirstMessage ? { stop() {
7744
- }, pause() {
7745
- }, resume() {
7746
- } } : startStatusWatcher({
7747
- apiConfig,
7748
- getContext: () => {
7749
- const parts = [];
7750
- const toolName = currentToolNames || getToolCalls(contentBlocks).filter((tc) => !STATUS_EXCLUDED_TOOLS.has(tc.name)).at(-1)?.name || lastCompletedTools;
7751
- if (toolName) {
7752
- parts.push(`Tool: ${toolName}`);
7753
- }
7754
- if (lastCompletedInput) {
7755
- parts.push(`Tool input: ${lastCompletedInput.slice(-1500)}`);
7756
- }
7757
- if (lastCompletedResult) {
7758
- parts.push(`Tool result: ${lastCompletedResult.slice(-1500)}`);
7759
- }
7760
- const text = subAgentText || getTextContent(contentBlocks).slice(-2e3);
7761
- if (text) {
7762
- parts.push(`Assistant text: ${text}`);
7763
- }
7764
- if (onboardingState && onboardingState !== "onboardingFinished") {
7765
- parts.push(`Build phase: ${onboardingState}`);
7950
+ const statusWatcher = isFirstMessage ? { stop() {
7951
+ }, pause() {
7952
+ }, resume() {
7953
+ } } : startStatusWatcher({
7954
+ apiConfig,
7955
+ getContext: () => {
7956
+ const parts = [];
7957
+ const toolName = currentToolNames || getToolCalls(contentBlocksRef).filter((tc) => !STATUS_EXCLUDED_TOOLS.has(tc.name)).at(-1)?.name || lastCompletedTools;
7958
+ if (toolName) {
7959
+ parts.push(`Tool: ${toolName}`);
7960
+ }
7961
+ const toolInput = sanitizeStatusText(lastCompletedInput);
7962
+ if (toolInput) {
7963
+ parts.push(`Tool input: ${toolInput.slice(-1500)}`);
7964
+ }
7965
+ const toolResult = sanitizeStatusText(lastCompletedResult);
7966
+ if (toolResult) {
7967
+ parts.push(`Tool result: ${toolResult.slice(-1500)}`);
7968
+ }
7969
+ const text = subAgentText || getTextContent(contentBlocksRef).slice(-2e3);
7970
+ if (text) {
7971
+ parts.push(`Assistant text: ${text}`);
7972
+ }
7973
+ if (onboardingState && onboardingState !== "onboardingFinished") {
7974
+ parts.push(`Build phase: ${onboardingState}`);
7975
+ }
7976
+ let hasUserSignal = false;
7977
+ for (const entry of keptEntries) {
7978
+ if (entry.hidden) {
7979
+ continue;
7766
7980
  }
7767
- for (const entry of keptEntries) {
7768
- const automated = parseSentinel(entry.text);
7769
- if (automated) {
7981
+ const automated = parseSentinel(entry.text);
7982
+ if (automated) {
7983
+ if (!NON_ACTION_SENTINELS.has(automated.name)) {
7770
7984
  parts.push(`Automated action: ${automated.name}`);
7771
- } else if (entry.text) {
7772
- parts.push(`User request: ${entry.text.slice(-500)}`);
7985
+ hasUserSignal = true;
7773
7986
  }
7987
+ } else if (entry.text) {
7988
+ parts.push(`User request: ${entry.text.slice(-500)}`);
7989
+ hasUserSignal = true;
7774
7990
  }
7775
- return parts.join("\n");
7776
- },
7777
- onStatus: (label) => onEvent({ type: "status", message: label }),
7778
- signal
7779
- });
7780
- async function handlePartialInput(acc, id, name, partial) {
7781
- const tool = getToolByName(name);
7782
- if (!tool?.streaming) {
7783
- return;
7784
7991
  }
7785
- const {
7786
- contentField = "content",
7787
- transform,
7788
- partialInput
7789
- } = tool.streaming;
7790
- if (partialInput) {
7791
- const result = partialInput(partial, acc.lastEmittedCount);
7792
- if (!result) {
7793
- return;
7794
- }
7795
- acc.lastEmittedCount = result.emittedCount;
7796
- acc.started = true;
7797
- onEvent({
7798
- type: "tool_start",
7799
- id,
7800
- name,
7801
- input: result.input,
7802
- partial: true
7803
- });
7804
- return;
7992
+ if (!hasUserSignal && !toolName && !text) {
7993
+ return "";
7805
7994
  }
7806
- const content = partial[contentField];
7807
- if (typeof content !== "string") {
7995
+ return parts.join("\n");
7996
+ },
7997
+ onStatus: (label) => onEvent({ type: "status", message: label }),
7998
+ signal
7999
+ });
8000
+ try {
8001
+ while (true) {
8002
+ let getOrCreateAccumulator2 = function(id, name) {
8003
+ let acc = toolInputAccumulators.get(id);
8004
+ if (!acc) {
8005
+ acc = { name, json: "", started: false, lastEmittedCount: 0 };
8006
+ toolInputAccumulators.set(id, acc);
8007
+ }
8008
+ return acc;
8009
+ };
8010
+ var getOrCreateAccumulator = getOrCreateAccumulator2;
8011
+ if (signal?.aborted) {
8012
+ onEvent({ type: "turn_cancelled" });
8013
+ saveSession(state);
7808
8014
  return;
7809
8015
  }
7810
- if (!acc.started) {
7811
- acc.started = true;
7812
- onEvent({ type: "tool_start", id, name, input: partial });
7813
- }
7814
- if (transform) {
7815
- const result = await transform(partial);
7816
- if (result === null) {
8016
+ const iterStart = Date.now();
8017
+ const contentBlocks = [];
8018
+ const thinkingBlockStartTimes = [];
8019
+ let thinkingCompleteCount = 0;
8020
+ let lastThinkingRelatedStartedAt;
8021
+ let textBlockOpen = false;
8022
+ const toolInputAccumulators = /* @__PURE__ */ new Map();
8023
+ let stopReason = "end_turn";
8024
+ let turnProviderMetadata;
8025
+ let turnModelId;
8026
+ contentBlocksRef = contentBlocks;
8027
+ subAgentText = "";
8028
+ currentToolNames = "";
8029
+ async function handlePartialInput(acc, id, name, partial) {
8030
+ const tool = getToolByName(name);
8031
+ if (!tool?.streaming) {
7817
8032
  return;
7818
8033
  }
7819
- onEvent({ type: "tool_input_delta", id, name, result });
7820
- } else {
7821
- onEvent({ type: "tool_input_delta", id, name, result: content });
7822
- }
7823
- }
7824
- try {
7825
- for await (const event of streamChatWithRetry(
7826
- {
7827
- ...apiConfig,
7828
- model: parentModel,
7829
- requestId,
7830
- system,
7831
- messages: cleanMessagesForApi(state.messages),
7832
- tools: tools2,
7833
- signal
7834
- },
7835
- {
7836
- onRetry: (attempt) => {
7837
- onEvent({
7838
- type: "status",
7839
- message: `Lost connection, retrying (attempt ${attempt + 2} of 3)`
7840
- });
8034
+ const {
8035
+ contentField = "content",
8036
+ transform,
8037
+ partialInput
8038
+ } = tool.streaming;
8039
+ if (partialInput) {
8040
+ const result = partialInput(partial, acc.lastEmittedCount);
8041
+ if (!result) {
8042
+ return;
7841
8043
  }
8044
+ acc.lastEmittedCount = result.emittedCount;
8045
+ acc.started = true;
8046
+ onEvent({
8047
+ type: "tool_start",
8048
+ id,
8049
+ name,
8050
+ input: result.input,
8051
+ partial: true
8052
+ });
8053
+ return;
7842
8054
  }
7843
- )) {
7844
- if (signal?.aborted) {
7845
- break;
8055
+ const content = partial[contentField];
8056
+ if (typeof content !== "string") {
8057
+ return;
7846
8058
  }
7847
- switch (event.type) {
7848
- case "text": {
7849
- const lastBlock = contentBlocks.at(-1);
7850
- if (lastBlock?.type === "text" && textBlockOpen) {
7851
- lastBlock.text += event.text;
7852
- } else {
7853
- contentBlocks.push({
7854
- type: "text",
7855
- text: event.text,
7856
- startedAt: event.ts
7857
- });
7858
- }
7859
- textBlockOpen = true;
7860
- onEvent({ type: "text", text: event.text });
7861
- break;
8059
+ if (!acc.started) {
8060
+ acc.started = true;
8061
+ onEvent({ type: "tool_start", id, name, input: partial });
8062
+ }
8063
+ if (transform) {
8064
+ const result = await transform(partial);
8065
+ if (result === null) {
8066
+ return;
7862
8067
  }
7863
- case "thinking":
7864
- if (event.text === "") {
7865
- thinkingBlockStartTimes.push(event.ts);
7866
- textBlockOpen = false;
8068
+ onEvent({ type: "tool_input_delta", id, name, result });
8069
+ } else {
8070
+ onEvent({ type: "tool_input_delta", id, name, result: content });
8071
+ }
8072
+ }
8073
+ try {
8074
+ for await (const event of streamChatWithRetry(
8075
+ {
8076
+ ...apiConfig,
8077
+ model: parentModel,
8078
+ requestId,
8079
+ system,
8080
+ messages: cleanMessagesForApi(state.messages),
8081
+ tools: tools2,
8082
+ signal
8083
+ },
8084
+ {
8085
+ onRetry: (attempt) => {
8086
+ onEvent({
8087
+ type: "status",
8088
+ message: `Lost connection, retrying (attempt ${attempt + 2} of 3)`
8089
+ });
7867
8090
  }
7868
- onEvent({ type: "thinking", text: event.text });
7869
- break;
7870
- case "thinking_complete": {
7871
- const startedAt = thinkingBlockStartTimes[thinkingCompleteCount] ?? event.ts;
7872
- contentBlocks.push({
7873
- type: "thinking",
7874
- thinking: event.thinking,
7875
- signature: event.signature,
7876
- startedAt,
7877
- completedAt: event.ts
7878
- });
7879
- thinkingCompleteCount++;
7880
- lastThinkingRelatedStartedAt = startedAt;
7881
- break;
7882
8091
  }
7883
- case "redacted_thinking_complete": {
7884
- const startedAt = lastThinkingRelatedStartedAt !== void 0 ? lastThinkingRelatedStartedAt + 1 : event.ts;
7885
- contentBlocks.push({
7886
- type: "redacted_thinking",
7887
- data: event.data,
7888
- startedAt,
7889
- completedAt: event.ts
7890
- });
7891
- lastThinkingRelatedStartedAt = startedAt;
8092
+ )) {
8093
+ if (signal?.aborted) {
7892
8094
  break;
7893
8095
  }
7894
- case "tool_input_delta": {
7895
- const acc = getOrCreateAccumulator2(event.id, event.name);
7896
- acc.json += event.delta;
7897
- try {
7898
- const partial = parsePartialJson(acc.json);
7899
- await handlePartialInput(acc, event.id, event.name, partial);
7900
- } catch {
8096
+ switch (event.type) {
8097
+ case "text": {
8098
+ const lastBlock = contentBlocks.at(-1);
8099
+ if (lastBlock?.type === "text" && textBlockOpen) {
8100
+ lastBlock.text += event.text;
8101
+ } else {
8102
+ contentBlocks.push({
8103
+ type: "text",
8104
+ text: event.text,
8105
+ startedAt: event.ts
8106
+ });
8107
+ }
8108
+ textBlockOpen = true;
8109
+ onEvent({ type: "text", text: event.text });
8110
+ break;
7901
8111
  }
7902
- break;
7903
- }
7904
- case "tool_input_args": {
7905
- const acc = getOrCreateAccumulator2(event.id, event.name);
7906
- await handlePartialInput(acc, event.id, event.name, event.args);
7907
- break;
7908
- }
7909
- case "tool_use": {
7910
- const tool = getToolByName(event.name);
7911
- contentBlocks.push({
7912
- type: "tool",
7913
- id: event.id,
7914
- name: event.name,
7915
- input: event.input,
7916
- startedAt: event.ts,
7917
- ...(event.input.background || tool?.backgroundOnly) && {
7918
- background: true
8112
+ case "thinking":
8113
+ if (event.text === "") {
8114
+ thinkingBlockStartTimes.push(event.ts);
8115
+ textBlockOpen = false;
7919
8116
  }
7920
- });
7921
- const acc = toolInputAccumulators.get(event.id);
7922
- const wasStreamed = acc?.started ?? false;
7923
- const isInputStreaming = !!tool?.streaming?.partialInput;
7924
- log14.info("Tool received", {
7925
- requestId,
7926
- toolCallId: event.id,
7927
- name: event.name
7928
- });
7929
- if (!wasStreamed || isInputStreaming) {
7930
- onEvent({
7931
- type: "tool_start",
8117
+ onEvent({ type: "thinking", text: event.text });
8118
+ break;
8119
+ case "thinking_complete": {
8120
+ const startedAt = thinkingBlockStartTimes[thinkingCompleteCount] ?? event.ts;
8121
+ contentBlocks.push({
8122
+ type: "thinking",
8123
+ thinking: event.thinking,
8124
+ signature: event.signature,
8125
+ startedAt,
8126
+ completedAt: event.ts
8127
+ });
8128
+ thinkingCompleteCount++;
8129
+ lastThinkingRelatedStartedAt = startedAt;
8130
+ break;
8131
+ }
8132
+ case "redacted_thinking_complete": {
8133
+ const startedAt = lastThinkingRelatedStartedAt !== void 0 ? lastThinkingRelatedStartedAt + 1 : event.ts;
8134
+ contentBlocks.push({
8135
+ type: "redacted_thinking",
8136
+ data: event.data,
8137
+ startedAt,
8138
+ completedAt: event.ts
8139
+ });
8140
+ lastThinkingRelatedStartedAt = startedAt;
8141
+ break;
8142
+ }
8143
+ case "tool_input_delta": {
8144
+ const acc = getOrCreateAccumulator2(event.id, event.name);
8145
+ acc.json += event.delta;
8146
+ try {
8147
+ const partial = parsePartialJson(acc.json);
8148
+ await handlePartialInput(acc, event.id, event.name, partial);
8149
+ } catch {
8150
+ }
8151
+ break;
8152
+ }
8153
+ case "tool_input_args": {
8154
+ const acc = getOrCreateAccumulator2(event.id, event.name);
8155
+ await handlePartialInput(acc, event.id, event.name, event.args);
8156
+ break;
8157
+ }
8158
+ case "tool_use": {
8159
+ const tool = getToolByName(event.name);
8160
+ contentBlocks.push({
8161
+ type: "tool",
7932
8162
  id: event.id,
7933
8163
  name: event.name,
7934
- input: event.input
8164
+ input: event.input,
8165
+ startedAt: event.ts,
8166
+ ...(event.input.background || tool?.backgroundOnly) && {
8167
+ background: true
8168
+ }
8169
+ });
8170
+ const acc = toolInputAccumulators.get(event.id);
8171
+ const wasStreamed = acc?.started ?? false;
8172
+ const isInputStreaming = !!tool?.streaming?.partialInput;
8173
+ log14.info("Tool received", {
8174
+ requestId,
8175
+ toolCallId: event.id,
8176
+ name: event.name
7935
8177
  });
8178
+ if (!wasStreamed || isInputStreaming) {
8179
+ onEvent({
8180
+ type: "tool_start",
8181
+ id: event.id,
8182
+ name: event.name,
8183
+ input: event.input,
8184
+ // Mirror the content-block stamp above so the live event
8185
+ // matches what a history reload shows — without it the
8186
+ // frontend renders background rows as already complete.
8187
+ ...isBackgroundCall({
8188
+ name: event.name,
8189
+ input: event.input
8190
+ }) && { background: true }
8191
+ });
8192
+ }
8193
+ break;
7936
8194
  }
7937
- break;
8195
+ case "done":
8196
+ stopReason = event.stopReason;
8197
+ turnProviderMetadata = event.providerMetadata;
8198
+ turnModelId = event.modelId;
8199
+ turnLlmCalls++;
8200
+ lastCallInputTokens = event.usage.inputTokens;
8201
+ lastCallCacheCreation = event.usage.cacheCreationTokens ?? 0;
8202
+ lastCallCacheRead = event.usage.cacheReadTokens ?? 0;
8203
+ turnInputTokens += lastCallInputTokens;
8204
+ turnOutputTokens += event.usage.outputTokens;
8205
+ turnCacheCreation += lastCallCacheCreation;
8206
+ turnCacheRead += lastCallCacheRead;
8207
+ recordUsage({
8208
+ ts: Date.now(),
8209
+ requestId,
8210
+ agentName: "parent",
8211
+ modelId: event.modelId,
8212
+ inputTokens: event.usage.inputTokens,
8213
+ outputTokens: event.usage.outputTokens,
8214
+ cacheCreationTokens: event.usage.cacheCreationTokens,
8215
+ cacheReadTokens: event.usage.cacheReadTokens,
8216
+ cost: nanoToDollars(event.cost),
8217
+ billingEvents: event.billingEvents,
8218
+ durationMs: Date.now() - iterStart,
8219
+ toolNames: contentBlocks.filter(
8220
+ (b) => b.type === "tool"
8221
+ ).map((b) => b.name)
8222
+ });
8223
+ break;
8224
+ case "error":
8225
+ statusWatcher.stop();
8226
+ onEvent({
8227
+ type: "error",
8228
+ error: friendlyError(event.error),
8229
+ ...event.code ? { code: event.code } : {}
8230
+ });
8231
+ return;
7938
8232
  }
7939
- case "done":
7940
- stopReason = event.stopReason;
7941
- turnProviderMetadata = event.providerMetadata;
7942
- turnModelId = event.modelId;
7943
- turnLlmCalls++;
7944
- lastCallInputTokens = event.usage.inputTokens;
7945
- lastCallCacheCreation = event.usage.cacheCreationTokens ?? 0;
7946
- lastCallCacheRead = event.usage.cacheReadTokens ?? 0;
7947
- turnInputTokens += lastCallInputTokens;
7948
- turnOutputTokens += event.usage.outputTokens;
7949
- turnCacheCreation += lastCallCacheCreation;
7950
- turnCacheRead += lastCallCacheRead;
7951
- recordUsage({
7952
- ts: Date.now(),
7953
- requestId,
7954
- agentName: "parent",
7955
- modelId: event.modelId,
7956
- inputTokens: event.usage.inputTokens,
7957
- outputTokens: event.usage.outputTokens,
7958
- cacheCreationTokens: event.usage.cacheCreationTokens,
7959
- cacheReadTokens: event.usage.cacheReadTokens,
7960
- cost: nanoToDollars(event.cost),
7961
- billingEvents: event.billingEvents,
7962
- durationMs: Date.now() - iterStart,
7963
- toolNames: contentBlocks.filter(
7964
- (b) => b.type === "tool"
7965
- ).map((b) => b.name)
7966
- });
7967
- break;
7968
- case "error":
7969
- onEvent({
7970
- type: "error",
7971
- error: friendlyError(event.error),
7972
- ...event.code ? { code: event.code } : {}
7973
- });
7974
- return;
8233
+ }
8234
+ } catch (err) {
8235
+ if (signal?.aborted) {
8236
+ } else {
8237
+ throw err;
7975
8238
  }
7976
8239
  }
7977
- } catch (err) {
7978
8240
  if (signal?.aborted) {
7979
- } else {
7980
- throw err;
8241
+ statusWatcher.stop();
8242
+ if (contentBlocks.length > 0) {
8243
+ contentBlocks.push({
8244
+ type: "text",
8245
+ text: "\n\n(cancelled)",
8246
+ startedAt: Date.now()
8247
+ });
8248
+ state.messages.push({
8249
+ role: "assistant",
8250
+ content: [...contentBlocks].sort(
8251
+ (a, b) => a.startedAt - b.startedAt
8252
+ ),
8253
+ usage: {
8254
+ inputTokens: turnInputTokens,
8255
+ outputTokens: turnOutputTokens,
8256
+ cacheCreationTokens: turnCacheCreation || void 0,
8257
+ cacheReadTokens: turnCacheRead || void 0,
8258
+ llmCalls: turnLlmCalls
8259
+ },
8260
+ ...turnProviderMetadata && {
8261
+ providerMetadata: turnProviderMetadata
8262
+ },
8263
+ model: turnModelId ?? parentModel,
8264
+ ...modelOverride && { modelOverride }
8265
+ });
8266
+ }
8267
+ onEvent({ type: "turn_cancelled" });
8268
+ saveSession(state);
8269
+ return;
7981
8270
  }
7982
- }
7983
- if (signal?.aborted) {
7984
- statusWatcher.stop();
7985
8271
  if (contentBlocks.length > 0) {
7986
- contentBlocks.push({
7987
- type: "text",
7988
- text: "\n\n(cancelled)",
7989
- startedAt: Date.now()
7990
- });
7991
8272
  state.messages.push({
7992
8273
  role: "assistant",
7993
8274
  content: [...contentBlocks].sort((a, b) => a.startedAt - b.startedAt),
@@ -8005,229 +8286,212 @@ async function runTurn(params) {
8005
8286
  ...modelOverride && { modelOverride }
8006
8287
  });
8007
8288
  }
8008
- onEvent({ type: "turn_cancelled" });
8009
- saveSession(state);
8010
- return;
8011
- }
8012
- if (contentBlocks.length > 0) {
8013
- state.messages.push({
8014
- role: "assistant",
8015
- content: [...contentBlocks].sort((a, b) => a.startedAt - b.startedAt),
8016
- usage: {
8017
- inputTokens: turnInputTokens,
8018
- outputTokens: turnOutputTokens,
8019
- cacheCreationTokens: turnCacheCreation || void 0,
8020
- cacheReadTokens: turnCacheRead || void 0,
8021
- llmCalls: turnLlmCalls
8022
- },
8023
- ...turnProviderMetadata && { providerMetadata: turnProviderMetadata },
8024
- model: turnModelId ?? parentModel,
8025
- ...modelOverride && { modelOverride }
8026
- });
8027
- }
8028
- const toolCalls = getToolCalls(contentBlocks);
8029
- if (stopReason !== "tool_use" || toolCalls.length === 0) {
8030
- statusWatcher.stop();
8031
- saveSession(state);
8032
- onEvent({
8033
- type: "turn_done",
8034
- stats: {
8035
- inputTokens: turnInputTokens,
8036
- outputTokens: turnOutputTokens,
8037
- cacheCreationTokens: turnCacheCreation || void 0,
8038
- cacheReadTokens: turnCacheRead || void 0,
8039
- llmCalls: turnLlmCalls,
8040
- lastCallInputTokens,
8041
- lastCallCacheCreation: lastCallCacheCreation || void 0,
8042
- lastCallCacheRead: lastCallCacheRead || void 0
8043
- }
8044
- });
8045
- return;
8046
- }
8047
- log14.info("Tools executing", {
8048
- requestId,
8049
- count: toolCalls.length,
8050
- tools: toolCalls.map((tc) => tc.name)
8051
- });
8052
- currentToolNames = toolCalls.filter((tc) => !STATUS_EXCLUDED_TOOLS.has(tc.name)).map((tc) => tc.name).join(", ");
8053
- const wrappedOnEvent = (e) => {
8054
- if ("parentToolId" in e && e.parentToolId) {
8055
- if (e.type === "text") {
8056
- subAgentText = e.text;
8057
- } else if (e.type === "tool_start") {
8058
- subAgentText = `Using ${e.name}`;
8059
- }
8289
+ const toolCalls = getToolCalls(contentBlocks);
8290
+ if (stopReason !== "tool_use" || toolCalls.length === 0) {
8291
+ statusWatcher.stop();
8292
+ saveSession(state);
8293
+ onEvent({
8294
+ type: "turn_done",
8295
+ stats: {
8296
+ inputTokens: turnInputTokens,
8297
+ outputTokens: turnOutputTokens,
8298
+ cacheCreationTokens: turnCacheCreation || void 0,
8299
+ cacheReadTokens: turnCacheRead || void 0,
8300
+ llmCalls: turnLlmCalls,
8301
+ lastCallInputTokens,
8302
+ lastCallCacheCreation: lastCallCacheCreation || void 0,
8303
+ lastCallCacheRead: lastCallCacheRead || void 0
8304
+ }
8305
+ });
8306
+ return;
8060
8307
  }
8061
- onEvent(e);
8062
- };
8063
- const subAgentMessages = /* @__PURE__ */ new Map();
8064
- const results = await Promise.all(
8065
- toolCalls.map(async (tc) => {
8066
- if (signal?.aborted) {
8067
- return { id: tc.id, result: USER_CANCELLED_RESULT, isError: true };
8308
+ log14.info("Tools executing", {
8309
+ requestId,
8310
+ count: toolCalls.length,
8311
+ tools: toolCalls.map((tc) => tc.name)
8312
+ });
8313
+ currentToolNames = toolCalls.filter((tc) => !STATUS_EXCLUDED_TOOLS.has(tc.name)).map((tc) => tc.name).join(", ");
8314
+ const wrappedOnEvent = (e) => {
8315
+ if ("parentToolId" in e && e.parentToolId) {
8316
+ if (e.type === "text") {
8317
+ subAgentText = e.text;
8318
+ } else if (e.type === "tool_start") {
8319
+ subAgentText = `Using ${e.name}`;
8320
+ }
8068
8321
  }
8069
- const toolStart = Date.now();
8070
- let settle;
8071
- const resultPromise = new Promise((res) => {
8072
- settle = (result, isError) => res({ id: tc.id, result, isError });
8073
- });
8074
- let toolAbort = new AbortController();
8075
- let settled = false;
8076
- const safeSettle = (result, isError) => {
8077
- if (settled) {
8078
- return;
8322
+ onEvent(e);
8323
+ };
8324
+ const subAgentMessages = /* @__PURE__ */ new Map();
8325
+ const results = await Promise.all(
8326
+ toolCalls.map(async (tc) => {
8327
+ if (signal?.aborted) {
8328
+ return { id: tc.id, result: USER_CANCELLED_RESULT, isError: true };
8079
8329
  }
8080
- settled = true;
8081
- signal?.removeEventListener("abort", cascadeAbort);
8082
- settle(result, isError);
8083
- };
8084
- const cascadeAbort = () => {
8085
- toolAbort.abort();
8086
- safeSettle(USER_CANCELLED_RESULT, true);
8087
- };
8088
- signal?.addEventListener("abort", cascadeAbort, { once: true });
8089
- const run = async (input) => {
8090
- try {
8091
- let result;
8092
- if (EXTERNAL_TOOLS.has(tc.name) && resolveExternalTool) {
8093
- saveSession(state);
8094
- log14.info("Waiting for external tool result", {
8095
- requestId,
8096
- toolCallId: tc.id,
8097
- name: tc.name
8098
- });
8099
- const blocksUser = USER_BLOCKING_EXTERNAL_TOOLS.has(tc.name);
8100
- if (blocksUser) {
8101
- statusWatcher.pause();
8102
- }
8103
- try {
8104
- result = await resolveExternalTool(tc.id, tc.name, input);
8105
- } finally {
8330
+ const toolStart = Date.now();
8331
+ let settle;
8332
+ const resultPromise = new Promise((res) => {
8333
+ settle = (result, isError) => res({ id: tc.id, result, isError });
8334
+ });
8335
+ let toolAbort = new AbortController();
8336
+ let settled = false;
8337
+ const safeSettle = (result, isError) => {
8338
+ if (settled) {
8339
+ return;
8340
+ }
8341
+ settled = true;
8342
+ signal?.removeEventListener("abort", cascadeAbort);
8343
+ settle(result, isError);
8344
+ };
8345
+ const cascadeAbort = () => {
8346
+ toolAbort.abort();
8347
+ safeSettle(USER_CANCELLED_RESULT, true);
8348
+ };
8349
+ signal?.addEventListener("abort", cascadeAbort, { once: true });
8350
+ const run = async (input) => {
8351
+ try {
8352
+ let result;
8353
+ if (EXTERNAL_TOOLS.has(tc.name) && resolveExternalTool) {
8354
+ saveSession(state);
8355
+ log14.info("Waiting for external tool result", {
8356
+ requestId,
8357
+ toolCallId: tc.id,
8358
+ name: tc.name
8359
+ });
8360
+ const blocksUser = USER_BLOCKING_EXTERNAL_TOOLS.has(tc.name);
8106
8361
  if (blocksUser) {
8107
- statusWatcher.resume();
8362
+ statusWatcher.pause();
8108
8363
  }
8364
+ try {
8365
+ result = await resolveExternalTool(tc.id, tc.name, input);
8366
+ } finally {
8367
+ if (blocksUser) {
8368
+ statusWatcher.resume();
8369
+ }
8370
+ }
8371
+ } else {
8372
+ result = await executeTool(tc.name, input, {
8373
+ apiConfig,
8374
+ model,
8375
+ models: state.models,
8376
+ signal: toolAbort.signal,
8377
+ onEvent: wrappedOnEvent,
8378
+ resolveExternalTool,
8379
+ toolCallId: tc.id,
8380
+ requestId,
8381
+ onboardingState,
8382
+ subAgentMessages,
8383
+ conversationMessages: state.messages,
8384
+ toolRegistry,
8385
+ onBackgroundComplete,
8386
+ onLog: (line) => wrappedOnEvent({
8387
+ type: "tool_input_delta",
8388
+ id: tc.id,
8389
+ name: tc.name,
8390
+ result: line
8391
+ })
8392
+ });
8109
8393
  }
8110
- } else {
8111
- result = await executeTool(tc.name, input, {
8112
- apiConfig,
8113
- model,
8114
- models: state.models,
8115
- signal: toolAbort.signal,
8116
- onEvent: wrappedOnEvent,
8117
- resolveExternalTool,
8118
- toolCallId: tc.id,
8119
- requestId,
8120
- onboardingState,
8121
- subAgentMessages,
8122
- conversationMessages: state.messages,
8123
- toolRegistry,
8124
- onBackgroundComplete,
8125
- onLog: (line) => wrappedOnEvent({
8126
- type: "tool_input_delta",
8127
- id: tc.id,
8128
- name: tc.name,
8129
- result: line
8130
- })
8394
+ safeSettle(capToolResult(result), result.startsWith("Error"));
8395
+ } catch (err) {
8396
+ safeSettle(`Error: ${err.message}`, true);
8397
+ }
8398
+ };
8399
+ const entry = {
8400
+ id: tc.id,
8401
+ name: tc.name,
8402
+ input: tc.input,
8403
+ abortController: toolAbort,
8404
+ startedAt: toolStart,
8405
+ settle: safeSettle,
8406
+ rerun: (newInput) => {
8407
+ settled = false;
8408
+ toolAbort = new AbortController();
8409
+ signal?.addEventListener("abort", () => toolAbort.abort(), {
8410
+ once: true
8131
8411
  });
8412
+ entry.abortController = toolAbort;
8413
+ entry.input = newInput;
8414
+ run(newInput);
8132
8415
  }
8133
- safeSettle(capToolResult(result), result.startsWith("Error"));
8134
- } catch (err) {
8135
- safeSettle(`Error: ${err.message}`, true);
8416
+ };
8417
+ toolRegistry?.register(entry);
8418
+ run(tc.input);
8419
+ const r = await resultPromise;
8420
+ if (!isBackgroundCall(tc)) {
8421
+ toolRegistry?.unregister(tc.id);
8136
8422
  }
8137
- };
8138
- const entry = {
8139
- id: tc.id,
8140
- name: tc.name,
8141
- input: tc.input,
8142
- abortController: toolAbort,
8143
- startedAt: toolStart,
8144
- settle: safeSettle,
8145
- rerun: (newInput) => {
8146
- settled = false;
8147
- toolAbort = new AbortController();
8148
- signal?.addEventListener("abort", () => toolAbort.abort(), {
8149
- once: true
8150
- });
8151
- entry.abortController = toolAbort;
8152
- entry.input = newInput;
8153
- run(newInput);
8423
+ log14.info("Tool completed", {
8424
+ requestId,
8425
+ toolCallId: tc.id,
8426
+ name: tc.name,
8427
+ durationMs: Date.now() - toolStart,
8428
+ isError: r.isError
8429
+ });
8430
+ onEvent({
8431
+ type: "tool_done",
8432
+ id: tc.id,
8433
+ name: tc.name,
8434
+ result: r.result,
8435
+ isError: r.isError
8436
+ });
8437
+ if (!r.isError && BRAND_TRIGGERING_TOOLS.has(tc.name)) {
8438
+ triggerBrandExtraction(
8439
+ apiConfig,
8440
+ resolveModel("brandExtractor", state.models, model)
8441
+ );
8154
8442
  }
8155
- };
8156
- toolRegistry?.register(entry);
8157
- run(tc.input);
8158
- const r = await resultPromise;
8159
- if (!isBackgroundCall(tc)) {
8160
- toolRegistry?.unregister(tc.id);
8161
- }
8162
- log14.info("Tool completed", {
8163
- requestId,
8164
- toolCallId: tc.id,
8165
- name: tc.name,
8166
- durationMs: Date.now() - toolStart,
8167
- isError: r.isError
8168
- });
8169
- onEvent({
8170
- type: "tool_done",
8171
- id: tc.id,
8172
- name: tc.name,
8173
- result: r.result,
8174
- isError: r.isError
8175
- });
8176
- if (!r.isError && BRAND_TRIGGERING_TOOLS.has(tc.name)) {
8177
- triggerBrandExtraction(
8178
- apiConfig,
8179
- resolveModel("brandExtractor", state.models, model)
8180
- );
8181
- }
8182
- return r;
8183
- })
8184
- );
8185
- statusWatcher.stop();
8186
- for (const r of results) {
8187
- const block = contentBlocks.find(
8188
- (b) => b.type === "tool" && b.id === r.id
8443
+ return r;
8444
+ })
8189
8445
  );
8190
- if (block?.type === "tool") {
8191
- block.result = r.result;
8192
- block.isError = r.isError;
8193
- block.completedAt = Date.now();
8194
- const msgs = subAgentMessages.get(r.id);
8195
- if (msgs) {
8196
- block.subAgentMessages = msgs;
8446
+ for (const r of results) {
8447
+ const block = contentBlocks.find(
8448
+ (b) => b.type === "tool" && b.id === r.id
8449
+ );
8450
+ if (block?.type === "tool") {
8451
+ block.result = r.result;
8452
+ block.isError = r.isError;
8453
+ block.completedAt = Date.now();
8454
+ const msgs = subAgentMessages.get(r.id);
8455
+ if (msgs) {
8456
+ block.subAgentMessages = msgs;
8457
+ }
8197
8458
  }
8198
8459
  }
8199
- }
8200
- const lastNonExcluded = toolCalls.filter(
8201
- (tc) => !STATUS_EXCLUDED_TOOLS.has(tc.name)
8202
- );
8203
- lastCompletedTools = lastNonExcluded.map((tc) => tc.name).join(", ");
8204
- lastCompletedInput = JSON.stringify(lastNonExcluded.at(-1)?.input ?? {});
8205
- lastCompletedResult = results.at(-1)?.result ?? "";
8206
- for (const r of results) {
8207
- state.messages.push({
8208
- role: "user",
8209
- content: r.result,
8210
- toolCallId: r.id,
8211
- isToolError: r.isError
8212
- });
8213
- }
8214
- if (takeSteering && !signal?.aborted) {
8215
- const injected = (await takeSteering()).filter(
8216
- (e) => e.text.trim().length > 0 || (e.attachments?.length ?? 0) > 0
8460
+ const lastNonExcluded = toolCalls.filter(
8461
+ (tc) => !STATUS_EXCLUDED_TOOLS.has(tc.name)
8217
8462
  );
8218
- if (injected.length > 0) {
8219
- for (const entry of injected) {
8220
- appendEntry(entry);
8221
- keptEntries.push(entry);
8463
+ lastCompletedTools = lastNonExcluded.map((tc) => tc.name).join(", ");
8464
+ lastCompletedInput = JSON.stringify(lastNonExcluded.at(-1)?.input ?? {});
8465
+ lastCompletedResult = results.at(-1)?.result ?? "";
8466
+ for (const r of results) {
8467
+ state.messages.push({
8468
+ role: "user",
8469
+ content: r.result,
8470
+ toolCallId: r.id,
8471
+ isToolError: r.isError
8472
+ });
8473
+ }
8474
+ if (takeSteering && !signal?.aborted) {
8475
+ const injected = (await takeSteering()).filter(
8476
+ (e) => e.text.trim().length > 0 || (e.attachments?.length ?? 0) > 0
8477
+ );
8478
+ if (injected.length > 0) {
8479
+ for (const entry of injected) {
8480
+ appendEntry(entry);
8481
+ keptEntries.push(entry);
8482
+ }
8483
+ saveSession(state);
8222
8484
  }
8485
+ }
8486
+ if (signal?.aborted) {
8487
+ statusWatcher.stop();
8488
+ onEvent({ type: "turn_cancelled" });
8223
8489
  saveSession(state);
8490
+ return;
8224
8491
  }
8225
8492
  }
8226
- if (signal?.aborted) {
8227
- onEvent({ type: "turn_cancelled" });
8228
- saveSession(state);
8229
- return;
8230
- }
8493
+ } finally {
8494
+ statusWatcher.stop();
8231
8495
  }
8232
8496
  }
8233
8497
 
@@ -8511,62 +8775,6 @@ var MessageQueue = class {
8511
8775
  }
8512
8776
  };
8513
8777
 
8514
- // src/automatedActions/resolve.ts
8515
- var NON_ACTION_SENTINELS = /* @__PURE__ */ new Set(["background_results"]);
8516
- function resolveAction(text) {
8517
- const parsed = parseSentinel(text);
8518
- if (!parsed) {
8519
- return null;
8520
- }
8521
- const { name: triggerName, remainder } = parsed;
8522
- if (NON_ACTION_SENTINELS.has(triggerName)) {
8523
- return null;
8524
- }
8525
- let params = {};
8526
- if (remainder) {
8527
- try {
8528
- params = JSON.parse(remainder.split("\n")[0]);
8529
- } catch {
8530
- }
8531
- }
8532
- let body = readAsset("automatedActions", `${triggerName}.md`);
8533
- let next;
8534
- const fmMatch = body.match(/^---\s*\n([\s\S]*?)\n---/);
8535
- if (fmMatch) {
8536
- const nextMatch = fmMatch[1].match(/^\s*next:\s*(\w+)\s*$/m);
8537
- if (nextMatch) {
8538
- next = nextMatch[1];
8539
- }
8540
- }
8541
- body = body.replace(/^---[\s\S]*?---\s*/, "");
8542
- for (const [key, value] of Object.entries(params)) {
8543
- const str = typeof value === "string" ? value : JSON.stringify(value);
8544
- body = body.replaceAll(`{{${key}}}`, str);
8545
- }
8546
- return {
8547
- message: automatedMessage(triggerName, body),
8548
- next
8549
- };
8550
- }
8551
- function getActionChain(startName) {
8552
- const chain = [];
8553
- const seen = /* @__PURE__ */ new Set();
8554
- let name = startName;
8555
- while (name && !seen.has(name)) {
8556
- seen.add(name);
8557
- chain.push(name);
8558
- let body;
8559
- try {
8560
- body = readAsset("automatedActions", `${name}.md`);
8561
- } catch {
8562
- break;
8563
- }
8564
- const fm = body.match(/^---\s*\n([\s\S]*?)\n---/);
8565
- name = fm?.[1].match(/^\s*next:\s*(\w+)\s*$/m)?.[1];
8566
- }
8567
- return chain;
8568
- }
8569
-
8570
8778
  // src/headless/index.ts
8571
8779
  var log16 = createLogger("headless");
8572
8780
  var EXTERNAL_TOOL_TIMEOUT_MS = 3e5;
@@ -8622,6 +8830,13 @@ var HeadlessSession = class {
8622
8830
  earlyResults = /* @__PURE__ */ new Map();
8623
8831
  // Tool block updates from background completions (separate from the message queue)
8624
8832
  pendingBlockUpdates = [];
8833
+ /**
8834
+ * Id of the UI-only tool block synthesized for the in-flight user/gate
8835
+ * compaction (see the compaction listener). Model-invoked compactions have
8836
+ * a real block and never set this. Single slot — triggerCompaction is
8837
+ * single-flight.
8838
+ */
8839
+ syntheticCompactionId = null;
8625
8840
  // Tool lifecycle management — shared across all nesting depths
8626
8841
  toolRegistry = new ToolRegistry();
8627
8842
  // IO — accumulates stdin bytes between newline boundaries
@@ -8653,6 +8868,7 @@ var HeadlessSession = class {
8653
8868
  this.emit("queue_changed", { queuedMessages: this.queue.snapshot() });
8654
8869
  });
8655
8870
  this.passivePen = loadPassiveResults();
8871
+ this.persistStats();
8656
8872
  if (resumed) {
8657
8873
  this.emit("session_restored", {
8658
8874
  messageCount: this.state.messages.length,
@@ -8675,15 +8891,61 @@ var HeadlessSession = class {
8675
8891
  );
8676
8892
  this.sessionStats.compactionInProgress = true;
8677
8893
  this.persistStats();
8894
+ if (!event.toolCallId && (event.origin === "user" || event.origin === "gate")) {
8895
+ const id = `compact_${Date.now()}`;
8896
+ this.syntheticCompactionId = id;
8897
+ this.state.messages.push({
8898
+ role: "assistant",
8899
+ content: [
8900
+ {
8901
+ type: "tool",
8902
+ id,
8903
+ name: "compactConversation",
8904
+ input: {},
8905
+ startedAt: Date.now(),
8906
+ background: true,
8907
+ uiOnly: true,
8908
+ result: "Compaction started in the background."
8909
+ }
8910
+ ]
8911
+ });
8912
+ saveSession(this.state);
8913
+ this.onEvent({
8914
+ type: "tool_start",
8915
+ id,
8916
+ name: "compactConversation",
8917
+ input: {},
8918
+ background: true
8919
+ });
8920
+ this.onEvent({
8921
+ type: "tool_done",
8922
+ id,
8923
+ name: "compactConversation",
8924
+ result: "Compaction started in the background.",
8925
+ isError: false
8926
+ });
8927
+ }
8678
8928
  } else {
8679
8929
  const data = event.error ? { error: event.error } : {};
8680
8930
  this.emit("compaction_complete", data, event.requestId);
8931
+ if (this.syntheticCompactionId) {
8932
+ const id = this.syntheticCompactionId;
8933
+ this.syntheticCompactionId = null;
8934
+ this.onBackgroundComplete(
8935
+ id,
8936
+ "compactConversation",
8937
+ event.error ? `Error: ${event.error}` : formatSummariesResult(event.summaries ?? [])
8938
+ );
8939
+ }
8681
8940
  this.sessionStats.compactionInProgress = false;
8682
8941
  if (!event.error) {
8683
8942
  this.sessionStats.lastContextSize = 0;
8684
8943
  }
8685
8944
  this.sessionStats.messageCount = this.state.messages.length;
8686
8945
  this.persistStats();
8946
+ if (!this.running) {
8947
+ this.kickDrain();
8948
+ }
8687
8949
  }
8688
8950
  });
8689
8951
  process.stdin.setEncoding("utf-8");
@@ -8822,7 +9084,8 @@ var HeadlessSession = class {
8822
9084
  await triggerCompaction(this.state, this.config, {
8823
9085
  blocking: true,
8824
9086
  requestId,
8825
- model: this.opts.model
9087
+ model: this.opts.model,
9088
+ origin: "gate"
8826
9089
  });
8827
9090
  applyPendingSummaries(this.state);
8828
9091
  } catch {
@@ -8851,6 +9114,12 @@ var HeadlessSession = class {
8851
9114
  }
8852
9115
  return;
8853
9116
  }
9117
+ if (notify === "silent") {
9118
+ if (!this.running) {
9119
+ this.applyPendingBlockUpdates();
9120
+ }
9121
+ return;
9122
+ }
8854
9123
  this.queue.push({
8855
9124
  command: {
8856
9125
  action: "message",
@@ -9232,6 +9501,12 @@ var HeadlessSession = class {
9232
9501
  this.completedEmitted = false;
9233
9502
  this.lastCompleted = null;
9234
9503
  this.turnStart = Date.now();
9504
+ const inflight2 = getInflightCompaction();
9505
+ if (inflight2) {
9506
+ await inflight2.catch(() => {
9507
+ });
9508
+ }
9509
+ applyPendingSummaries(this.state);
9235
9510
  if (this.passivePen.length > 0) {
9236
9511
  const swept = this.passivePen.splice(0);
9237
9512
  this.persistStats();
@@ -9321,7 +9596,7 @@ var HeadlessSession = class {
9321
9596
  this.applyPendingBlockUpdates();
9322
9597
  }
9323
9598
  async handleMessage(parsed, requestId) {
9324
- if (this.running) {
9599
+ if (this.running || getInflightCompaction()) {
9325
9600
  const command = { ...parsed };
9326
9601
  if (requestId && command.requestId === void 0) {
9327
9602
  command.requestId = requestId;
@@ -9331,6 +9606,9 @@ var HeadlessSession = class {
9331
9606
  source: "user",
9332
9607
  enqueuedAt: Date.now()
9333
9608
  });
9609
+ if (!this.running && !getInflightCompaction()) {
9610
+ this.kickDrain();
9611
+ }
9334
9612
  return;
9335
9613
  }
9336
9614
  this.running = true;
@@ -9338,6 +9616,7 @@ var HeadlessSession = class {
9338
9616
  await this.runSingleTurn(parsed, requestId);
9339
9617
  await this.drainQueueLoop();
9340
9618
  } finally {
9619
+ this.currentAbort?.abort();
9341
9620
  this.currentAbort = null;
9342
9621
  this.currentRequestId = void 0;
9343
9622
  this.running = false;
@@ -9369,6 +9648,18 @@ var HeadlessSession = class {
9369
9648
  async drainQueueLoop() {
9370
9649
  while (this.queue.length > 0) {
9371
9650
  const head = this.queue.peek();
9651
+ if (head.command.action === "compact") {
9652
+ this.queue.shift();
9653
+ await triggerCompaction(this.state, this.config, {
9654
+ blocking: true,
9655
+ requestId: head.command.requestId,
9656
+ model: this.opts.model,
9657
+ origin: "user"
9658
+ }).catch(() => {
9659
+ });
9660
+ applyPendingSummaries(this.state);
9661
+ continue;
9662
+ }
9372
9663
  if (head.source === "chain") {
9373
9664
  const item = this.queue.shift();
9374
9665
  const rid = item.command.requestId ?? `chain-${Date.now()}`;
@@ -9413,6 +9704,7 @@ var HeadlessSession = class {
9413
9704
  try {
9414
9705
  await this.drainQueueLoop();
9415
9706
  } finally {
9707
+ this.currentAbort?.abort();
9416
9708
  this.currentAbort = null;
9417
9709
  this.currentRequestId = void 0;
9418
9710
  this.running = false;
@@ -9659,11 +9951,31 @@ var HeadlessSession = class {
9659
9951
  return;
9660
9952
  }
9661
9953
  if (action === "compact") {
9954
+ if (this.running) {
9955
+ const alreadyQueued = this.queue.snapshot().some((it) => it.command.action === "compact");
9956
+ if (!alreadyQueued) {
9957
+ this.queue.push({
9958
+ command: {
9959
+ action: "compact",
9960
+ // Display label for the queue card (matches the automated
9961
+ // action idiom); never resolved as an action — drainQueueLoop
9962
+ // routes on command.action before any sentinel handling.
9963
+ text: sentinel("compact"),
9964
+ requestId
9965
+ },
9966
+ source: "user",
9967
+ enqueuedAt: Date.now()
9968
+ });
9969
+ }
9970
+ this.emit("completed", { success: true, queued: true }, requestId);
9971
+ return;
9972
+ }
9662
9973
  try {
9663
9974
  await triggerCompaction(this.state, this.config, {
9664
9975
  blocking: false,
9665
9976
  requestId,
9666
- model: this.opts.model
9977
+ model: this.opts.model,
9978
+ origin: "user"
9667
9979
  });
9668
9980
  if (!this.running) {
9669
9981
  applyPendingSummaries(this.state);