@xelauvas/xela-cli 0.3.0 → 0.4.0

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 (136) hide show
  1. package/local/README.md +112 -0
  2. package/local/start-qwen.sh +47 -0
  3. package/local/start-tunnel.sh +36 -0
  4. package/local/xela-local.sh +139 -0
  5. package/package.json +19 -5
  6. package/run.sh +33 -0
  7. package/src/Tool.ts +1 -1
  8. package/src/_shims/_generated_stubs/_universal_stub.mjs +168 -0
  9. package/src/_shims/_generated_stubs/stub_346f96741420f62caece15b196e233c6.mjs +19 -0
  10. package/src/_shims/_generated_stubs/stub_4b79af2a6ac1af5a53a55fd666b2bbb4.mjs +3 -0
  11. package/src/_shims/_generated_stubs/stub_94345ddeadacc184ff895597ec5abf71.mjs +6 -0
  12. package/src/_shims/_generated_stubs/stub_99fa0bc272643c549d8a2ec6d578436f.mjs +3 -0
  13. package/src/_shims/_generated_stubs/stub_e3451b131fcaf272a2a75270b9a427a3.mjs +9 -0
  14. package/src/_shims/_generated_stubs/text_stub.mjs +1 -0
  15. package/src/_shims/bun_bundle.js +79 -4
  16. package/src/_shims/cjs_stub.cjs +32 -10
  17. package/src/_shims/loader.js +104 -5
  18. package/src/_shims/openai_adapter.ts +446 -230
  19. package/src/_shims/register.js +16 -3
  20. package/src/assistant/gate.ts +15 -0
  21. package/src/assistant/index.ts +89 -0
  22. package/src/bootstrap/state.ts +6 -1
  23. package/src/bridge/peerSessions.ts +18 -0
  24. package/src/cli/print.ts +5 -0
  25. package/src/commands/assistant/assistant.ts +17 -0
  26. package/src/commands/assistant/index.ts +11 -0
  27. package/src/commands/brief.ts +3 -1
  28. package/src/commands/buddy/buddy.ts +88 -0
  29. package/src/commands/buddy/index.ts +12 -0
  30. package/src/commands/model/model.tsx +8 -23
  31. package/src/commands/peers/index.ts +11 -0
  32. package/src/commands/peers/peers.ts +24 -0
  33. package/src/commands/proactive/proactive.ts +64 -0
  34. package/src/commands/proactive.ts +12 -0
  35. package/src/commands/workflows/index.ts +12 -0
  36. package/src/commands/workflows/workflows.ts +36 -0
  37. package/src/commands.ts +4 -8
  38. package/src/components/Messages.tsx +1 -1
  39. package/src/components/ModelPicker.tsx +4 -34
  40. package/src/components/Spinner.tsx +54 -1
  41. package/src/components/StructuredDiff/colorDiff.ts +3 -1
  42. package/src/components/messages/SnipBoundaryMessage.tsx +15 -0
  43. package/src/components/messages/UserCrossSessionMessage.tsx +26 -0
  44. package/src/components/permissions/PermissionRequest.tsx +4 -2
  45. package/src/components/permissions/ReviewArtifactPermissionRequest/ReviewArtifactPermissionRequest.tsx +96 -0
  46. package/src/constants/prompts.ts +26 -0
  47. package/src/ink/terminal.ts +9 -1
  48. package/src/jobs/classifier.ts +103 -0
  49. package/src/memdir/paths.ts +13 -1
  50. package/src/proactive/index.ts +158 -0
  51. package/src/proactive/useProactive.ts +78 -0
  52. package/src/screens/REPL.tsx +28 -8
  53. package/src/services/api/claude.ts +6 -1
  54. package/src/services/compact/autoCompact.ts +18 -4
  55. package/src/services/compact/cachedMCConfig.ts +1 -0
  56. package/src/services/compact/cachedMicrocompact.ts +113 -0
  57. package/src/services/compact/compact.ts +22 -2
  58. package/src/services/compact/reactiveCompact.ts +251 -0
  59. package/src/services/compact/snipCompact.ts +282 -0
  60. package/src/services/compact/snipProjection.ts +50 -0
  61. package/src/services/contextCollapse/index.ts +322 -0
  62. package/src/services/contextCollapse/operations.ts +187 -0
  63. package/src/services/contextCollapse/persist.ts +40 -0
  64. package/src/services/extractMemories/extractMemories.ts +11 -2
  65. package/src/services/skillSearch/featureCheck.ts +12 -0
  66. package/src/services/skillSearch/localSearch.ts +98 -0
  67. package/src/services/skillSearch/prefetch.ts +125 -0
  68. package/src/services/skillSearch/remoteSkillLoader.ts +9 -0
  69. package/src/services/skillSearch/remoteSkillState.ts +21 -0
  70. package/src/services/skillSearch/signals.ts +7 -0
  71. package/src/services/skillSearch/telemetry.ts +9 -0
  72. package/src/setup.ts +10 -5
  73. package/src/skills/bundled/dream.ts +31 -0
  74. package/src/skills/bundled/hunter.ts +45 -0
  75. package/src/skills/bundled/index.ts +10 -23
  76. package/src/skills/bundled/runSkillGenerator.ts +61 -0
  77. package/src/skills/mcpSkills.ts +101 -0
  78. package/src/tasks/LocalWorkflowTask/LocalWorkflowTask.ts +301 -0
  79. package/src/tasks/MonitorMcpTask/MonitorMcpTask.ts +273 -0
  80. package/src/tasks.ts +22 -8
  81. package/src/tools/AgentTool/runAgent.ts +11 -0
  82. package/src/tools/BriefTool/BriefTool.ts +2 -0
  83. package/src/tools/CtxInspectTool/CtxInspectTool.ts +102 -0
  84. package/src/tools/DiscoverSkillsTool/DiscoverSkillsTool.ts +78 -0
  85. package/src/tools/DiscoverSkillsTool/prompt.ts +3 -0
  86. package/src/tools/ListPeersTool/ListPeersTool.ts +89 -0
  87. package/src/tools/MCPTool/MCPTool.ts +6 -0
  88. package/src/tools/MonitorTool/MonitorTool.ts +273 -0
  89. package/src/tools/MonitorTool/UI.tsx +25 -0
  90. package/src/tools/MonitorTool/events.ts +84 -0
  91. package/src/tools/MonitorTool/prompt.ts +45 -0
  92. package/src/tools/OverflowTestTool/OverflowTestTool.ts +73 -0
  93. package/src/tools/PushNotificationTool/PushNotificationTool.ts +119 -0
  94. package/src/tools/ReviewArtifactTool/ReviewArtifactTool.ts +93 -0
  95. package/src/tools/ReviewArtifactTool/prompt.ts +5 -0
  96. package/src/tools/SendUserFileTool/SendUserFileTool.ts +87 -0
  97. package/src/tools/SendUserFileTool/prompt.ts +7 -0
  98. package/src/tools/SleepTool/SleepTool.ts +126 -0
  99. package/src/tools/SnipTool/SnipTool.ts +90 -0
  100. package/src/tools/SnipTool/prompt.ts +5 -0
  101. package/src/tools/SubscribePRTool/SubscribePRTool.ts +115 -0
  102. package/src/tools/TerminalCaptureTool/TerminalCaptureTool.ts +129 -0
  103. package/src/tools/TerminalCaptureTool/prompt.ts +8 -0
  104. package/src/tools/ToolSearchTool/prompt.ts +1 -1
  105. package/src/tools/VerifyPlanExecutionTool/VerifyPlanExecutionTool.ts +99 -0
  106. package/src/tools/WebBrowserTool/WebBrowserPanel.tsx +10 -0
  107. package/src/tools/WebBrowserTool/WebBrowserTool.ts +190 -0
  108. package/src/tools/WebBrowserTool/prompt.ts +16 -0
  109. package/src/tools/WorkflowTool/UI.tsx +27 -0
  110. package/src/tools/WorkflowTool/WorkflowTool.ts +401 -0
  111. package/src/tools/WorkflowTool/bundled/index.ts +88 -0
  112. package/src/tools/WorkflowTool/constants.ts +7 -0
  113. package/src/tools/WorkflowTool/createWorkflowCommand.ts +84 -0
  114. package/src/tools/WorkflowTool/prompt.ts +38 -0
  115. package/src/tools/WorkflowTool/runtime.ts +331 -0
  116. package/src/tools.ts +36 -31
  117. package/src/utils/betas.ts +5 -0
  118. package/src/utils/context.ts +9 -0
  119. package/src/utils/conversationRecovery.ts +1 -1
  120. package/src/utils/forkedAgent.ts +4 -0
  121. package/src/utils/logoV2Utils.ts +7 -3
  122. package/src/utils/messageQueueManager.ts +23 -9
  123. package/src/utils/model/model.ts +10 -9
  124. package/src/utils/model/modelOptions.ts +0 -21
  125. package/src/utils/model/validateModel.ts +0 -19
  126. package/src/utils/permissions/permissionSetup.ts +5 -0
  127. package/src/utils/permissions/permissions.ts +25 -0
  128. package/src/utils/permissions/yolo-classifier-prompts/auto_mode_system_prompt.txt +38 -0
  129. package/src/utils/permissions/yolo-classifier-prompts/permissions_external.txt +25 -0
  130. package/src/utils/permissions/yoloClassifier.ts +28 -6
  131. package/src/utils/plugins/officialMarketplaceStartupCheck.ts +9 -0
  132. package/src/utils/systemThemeWatcher.ts +62 -0
  133. package/src/utils/udsClient.ts +115 -0
  134. package/src/utils/udsMessaging.ts +157 -0
  135. package/src/utils/yaml.ts +251 -5
  136. package/src/utils/model/providerModels.ts +0 -168
@@ -0,0 +1,158 @@
1
+ import { TICK_TAG } from '../constants/xml.js'
2
+
3
+ /**
4
+ * Proactive mode: the model keeps working between user prompts. The session
5
+ * receives periodic <tick> prompts (interactive: useProactive in the REPL;
6
+ * headless: print.ts scheduleProactiveTick) and either does something useful
7
+ * or calls Sleep, which schedules the next tick. State lives on globalThis
8
+ * because this module is reached both through ESM imports and the CJS
9
+ * require shim, and React reads it through subscribeToProactiveChanges.
10
+ */
11
+ export type ProactiveSource = 'command' | 'flag' | 'sdk' | 'env'
12
+
13
+ type ProactiveState = {
14
+ active: boolean
15
+ paused: boolean
16
+ contextBlocked: boolean
17
+ source: ProactiveSource | null
18
+ activatedAt: number | null
19
+ tickCount: number
20
+ lastTickAt: number | null
21
+ /** When the next tick is due (set by Sleep or the ticker), null when none is scheduled. */
22
+ nextTickAt: number | null
23
+ listeners: Set<() => void>
24
+ }
25
+
26
+ const state: ProactiveState = ((globalThis as unknown as { __xelaProactive?: ProactiveState })
27
+ .__xelaProactive ??= {
28
+ active: false,
29
+ paused: false,
30
+ contextBlocked: false,
31
+ source: null,
32
+ activatedAt: null,
33
+ tickCount: 0,
34
+ lastTickAt: null,
35
+ nextTickAt: null,
36
+ listeners: new Set(),
37
+ })
38
+
39
+ /** Default spacing between ticks when the model does not Sleep itself. */
40
+ export const PROACTIVE_TICK_INTERVAL_MS = Number(
41
+ process.env.CLAUDE_CODE_PROACTIVE_TICK_MS || 5 * 60_000,
42
+ )
43
+ /** First tick shortly after activation so the model greets and starts. */
44
+ export const PROACTIVE_FIRST_TICK_MS = 1_500
45
+
46
+ function notify(): void {
47
+ for (const l of state.listeners) {
48
+ try {
49
+ l()
50
+ } catch {
51
+ // listener errors must not break state updates
52
+ }
53
+ }
54
+ }
55
+
56
+ export function subscribeToProactiveChanges(listener: () => void): () => void {
57
+ state.listeners.add(listener)
58
+ return () => {
59
+ state.listeners.delete(listener)
60
+ }
61
+ }
62
+
63
+ export function isProactiveActive(): boolean {
64
+ return state.active
65
+ }
66
+
67
+ export function isProactivePaused(): boolean {
68
+ return state.paused || state.contextBlocked
69
+ }
70
+
71
+ export function isContextBlocked(): boolean {
72
+ return state.contextBlocked
73
+ }
74
+
75
+ export function getNextTickAt(): number | null {
76
+ return state.active && !isProactivePaused() ? state.nextTickAt : null
77
+ }
78
+
79
+ export function getProactiveState(): Readonly<Omit<ProactiveState, 'listeners'>> {
80
+ const { listeners: _l, ...rest } = state
81
+ return rest
82
+ }
83
+
84
+ export function activateProactive(source: ProactiveSource = 'command'): void {
85
+ state.active = true
86
+ state.paused = false
87
+ state.contextBlocked = false
88
+ state.source = source
89
+ state.activatedAt = Date.now()
90
+ state.nextTickAt = Date.now() + PROACTIVE_FIRST_TICK_MS
91
+ notify()
92
+ }
93
+
94
+ export function deactivateProactive(): void {
95
+ state.active = false
96
+ state.paused = false
97
+ state.source = null
98
+ state.nextTickAt = null
99
+ notify()
100
+ }
101
+
102
+ export function pauseProactive(): void {
103
+ if (!state.paused) {
104
+ state.paused = true
105
+ notify()
106
+ }
107
+ }
108
+
109
+ export function resumeProactive(): void {
110
+ if (state.paused || state.contextBlocked) {
111
+ state.paused = false
112
+ state.contextBlocked = false
113
+ notify()
114
+ }
115
+ }
116
+
117
+ /**
118
+ * Set when the context is too full to keep ticking (prompt-too-long with
119
+ * nothing left to compact). Cleared by /clear and by resumeProactive.
120
+ */
121
+ export function setContextBlocked(blocked: boolean): void {
122
+ if (state.contextBlocked !== blocked) {
123
+ state.contextBlocked = blocked
124
+ notify()
125
+ }
126
+ }
127
+
128
+ /** Schedule the next tick `ms` from now (Sleep tool, ticker). */
129
+ export function scheduleNextTick(ms: number): number {
130
+ const at = Date.now() + Math.max(0, ms)
131
+ state.nextTickAt = at
132
+ notify()
133
+ return at
134
+ }
135
+
136
+ export function clearNextTick(): void {
137
+ if (state.nextTickAt !== null) {
138
+ state.nextTickAt = null
139
+ notify()
140
+ }
141
+ }
142
+
143
+ export function recordTick(): number {
144
+ state.tickCount++
145
+ state.lastTickAt = Date.now()
146
+ state.nextTickAt = null
147
+ notify()
148
+ return state.tickCount
149
+ }
150
+
151
+ /** The prompt injected on every tick: the user's local time, as in headless mode. */
152
+ export function getTickPrompt(): string {
153
+ return `<${TICK_TAG}>${new Date().toLocaleTimeString()}</${TICK_TAG}>`
154
+ }
155
+
156
+ export function isTickPrompt(text: string): boolean {
157
+ return new RegExp(`^<${TICK_TAG}\\b`).test(text.trimStart())
158
+ }
@@ -0,0 +1,78 @@
1
+ import { useEffect, useRef, useSyncExternalStore } from 'react'
2
+ import { logForDebugging } from '../utils/debug.js'
3
+ import {
4
+ getNextTickAt,
5
+ getTickPrompt,
6
+ isProactiveActive,
7
+ isProactivePaused,
8
+ PROACTIVE_TICK_INTERVAL_MS,
9
+ recordTick,
10
+ scheduleNextTick,
11
+ subscribeToProactiveChanges,
12
+ } from './index.js'
13
+
14
+ type Props = {
15
+ isLoading: boolean
16
+ queuedCommandsLength: number
17
+ hasActiveLocalJsxUI: boolean
18
+ isInPlanMode: boolean
19
+ /** Submit a tick prompt right away (idle REPL). */
20
+ onSubmitTick: (prompt: string) => void
21
+ /** Queue a tick prompt behind the current turn (busy REPL). */
22
+ onQueueTick: (prompt: string) => void
23
+ }
24
+
25
+ /**
26
+ * Interactive tick driver. While proactive mode is active and not paused,
27
+ * fires a <tick> prompt when the scheduled time arrives (set by Sleep or,
28
+ * failing that, the default interval). Ticks wait while a turn is running,
29
+ * while commands are queued, while a local dialog is open, or in plan mode.
30
+ */
31
+ export function useProactive({
32
+ isLoading,
33
+ queuedCommandsLength,
34
+ hasActiveLocalJsxUI,
35
+ isInPlanMode,
36
+ onSubmitTick,
37
+ onQueueTick,
38
+ }: Props): void {
39
+ const active = useSyncExternalStore(subscribeToProactiveChanges, isProactiveActive)
40
+ const paused = useSyncExternalStore(subscribeToProactiveChanges, isProactivePaused)
41
+ const nextTickAt = useSyncExternalStore(subscribeToProactiveChanges, getNextTickAt)
42
+
43
+ const latest = useRef({ isLoading, queuedCommandsLength, hasActiveLocalJsxUI, isInPlanMode, onSubmitTick, onQueueTick })
44
+ latest.current = { isLoading, queuedCommandsLength, hasActiveLocalJsxUI, isInPlanMode, onSubmitTick, onQueueTick }
45
+
46
+ // Nothing scheduled while active and idle: fall back to the default interval
47
+ // so a turn that ended without Sleep still gets a follow-up.
48
+ useEffect(() => {
49
+ if (!active || paused || nextTickAt !== null || isLoading) return
50
+ scheduleNextTick(PROACTIVE_TICK_INTERVAL_MS)
51
+ }, [active, paused, nextTickAt, isLoading])
52
+
53
+ useEffect(() => {
54
+ if (!active || paused || nextTickAt === null) return
55
+ const fire = (): void => {
56
+ const l = latest.current
57
+ if (!isProactiveActive() || isProactivePaused()) {
58
+ logForDebugging('[proactive] tick skipped: inactive or paused')
59
+ return
60
+ }
61
+ if (l.hasActiveLocalJsxUI || l.isInPlanMode) {
62
+ // Try again shortly; the user is in a dialog or planning.
63
+ logForDebugging('[proactive] tick deferred: dialog open or plan mode')
64
+ scheduleNextTick(15_000)
65
+ return
66
+ }
67
+ const n = recordTick()
68
+ const prompt = getTickPrompt()
69
+ logForDebugging(`[proactive] tick ${n} ${l.isLoading || l.queuedCommandsLength > 0 ? 'queued' : 'submitted'}`)
70
+ if (l.isLoading || l.queuedCommandsLength > 0) l.onQueueTick(prompt)
71
+ else l.onSubmitTick(prompt)
72
+ }
73
+ const delay = Math.max(0, nextTickAt - Date.now())
74
+ logForDebugging(`[proactive] next tick in ${Math.round(delay / 1000)}s`)
75
+ const timer = setTimeout(fire, delay)
76
+ return () => clearTimeout(timer)
77
+ }, [active, paused, nextTickAt])
78
+ }
@@ -1,4 +1,9 @@
1
1
  import { c as _c } from "react/compiler-runtime";
2
+ // Static imports (not require): these touch React context / shared state and
3
+ // must be the same module instance as the rest of the ESM graph.
4
+ import * as proactiveModuleImpl from '../proactive/index.js';
5
+ import { useProactive as useProactiveImpl } from '../proactive/useProactive.js';
6
+ import { useScheduledTasks as useScheduledTasksImpl } from '../hooks/useScheduledTasks.js';
2
7
  // biome-ignore-all assist/source/organizeImports: ANT-ONLY import markers must not be reordered
3
8
  import { feature } from 'bun:bundle';
4
9
  import { spawnSync } from 'child_process';
@@ -67,6 +72,7 @@ import { SkillImprovementSurvey } from '../components/SkillImprovementSurvey.js'
67
72
  import { useSkillImprovementSurvey } from '../hooks/useSkillImprovementSurvey.js';
68
73
  import { useMoreRight } from '../moreright/useMoreRight.js';
69
74
  import { SpinnerWithVerb, BriefIdleStatus, type SpinnerMode } from '../components/Spinner.js';
75
+ import type { CompactProgressInfo } from '../components/Spinner.js';
70
76
  import { getSystemPrompt } from '../constants/prompts.js';
71
77
  import { buildEffectiveSystemPrompt } from '../utils/systemPrompt.js';
72
78
  import { getSystemContext, getUserContext } from '../context.js';
@@ -191,12 +197,12 @@ import { restoreRemoteAgentTasks } from '../tasks/RemoteAgentTask/RemoteAgentTas
191
197
  import { useInboxPoller } from '../hooks/useInboxPoller.js';
192
198
  // Dead code elimination: conditional import for loop mode
193
199
  /* eslint-disable @typescript-eslint/no-require-imports */
194
- const proactiveModule = feature('PROACTIVE') || feature('KAIROS') ? require('../proactive/index.js') : null;
200
+ const proactiveModule = feature('PROACTIVE') || feature('KAIROS') ? proactiveModuleImpl : null;
195
201
  const PROACTIVE_NO_OP_SUBSCRIBE = (_cb: () => void) => () => {};
196
202
  const PROACTIVE_FALSE = () => false;
197
203
  const SUGGEST_BG_PR_NOOP = (_p: string, _n: string): boolean => false;
198
- const useProactive = feature('PROACTIVE') || feature('KAIROS') ? require('../proactive/useProactive.js').useProactive : null;
199
- const useScheduledTasks = feature('AGENT_TRIGGERS') ? require('../hooks/useScheduledTasks.js').useScheduledTasks : null;
204
+ const useProactive = feature('PROACTIVE') || feature('KAIROS') ? useProactiveImpl : null;
205
+ const useScheduledTasks = feature('AGENT_TRIGGERS') ? useScheduledTasksImpl : null;
200
206
  /* eslint-enable @typescript-eslint/no-require-imports */
201
207
  import { isAgentSwarmsEnabled } from '../utils/agentSwarmsEnabled.js';
202
208
  import { useTaskListWatcher } from '../hooks/useTaskListWatcher.js';
@@ -1473,6 +1479,9 @@ export function REPL({
1473
1479
  const visibleStreamingText = streamingText && showStreamingText ? streamingText.substring(0, streamingText.lastIndexOf('\n') + 1) || null : null;
1474
1480
  const [lastQueryCompletionTime, setLastQueryCompletionTime] = useState(0);
1475
1481
  const [spinnerMessage, setSpinnerMessage] = useState<string | null>(null);
1482
+ // Set while a compaction summary is being generated; drives the progress
1483
+ // bar under the spinner (see CompactProgressBar in Spinner.tsx).
1484
+ const [compactProgress, setCompactProgress] = useState<CompactProgressInfo | null>(null);
1476
1485
  const [spinnerColor, setSpinnerColor] = useState<keyof Theme | null>(null);
1477
1486
  const [spinnerShimmerColor, setSpinnerShimmerColor] = useState<keyof Theme | null>(null);
1478
1487
  const [isMessageSelectorVisible, setIsMessageSelectorVisible] = useState(false);
@@ -1576,6 +1585,7 @@ export function REPL({
1576
1585
  setStreamingText(null);
1577
1586
  setStreamingToolUses([]);
1578
1587
  setSpinnerMessage(null);
1588
+ setCompactProgress(null);
1579
1589
  setSpinnerColor(null);
1580
1590
  setSpinnerShimmerColor(null);
1581
1591
  pickNewSpinnerTip();
@@ -2503,9 +2513,14 @@ export function REPL({
2503
2513
  break;
2504
2514
  case 'compact_start':
2505
2515
  setSpinnerMessage('Compacting conversation');
2516
+ setCompactProgress({
2517
+ startedAt: Date.now(),
2518
+ preCompactTokenCount: event.preCompactTokenCount ?? 0
2519
+ });
2506
2520
  break;
2507
2521
  case 'compact_end':
2508
2522
  setSpinnerMessage(null);
2523
+ setCompactProgress(null);
2509
2524
  setSpinnerColor(null);
2510
2525
  setSpinnerShimmerColor(null);
2511
2526
  break;
@@ -3062,7 +3077,7 @@ export function REPL({
3062
3077
  }
3063
3078
 
3064
3079
  // Atomically: clear initial message, set permission mode and rules, and store plan for verification
3065
- const shouldStorePlanForVerification = initialMsg.message.planContent && "external" === 'ant' && isEnvTruthy(undefined);
3080
+ const shouldStorePlanForVerification = initialMsg.message.planContent && process.env.CLAUDE_CODE_VERIFY_PLAN === 'true';
3066
3081
  setAppState(prev => {
3067
3082
  // Build and apply permission updates (mode + allowedPrompts rules)
3068
3083
  let updatedToolPermissionContext = initialMsg.mode ? applyPermissionUpdates(prev.toolPermissionContext, buildPermissionUpdates(initialMsg.mode, initialMsg.allowedPrompts)) : prev.toolPermissionContext;
@@ -4073,10 +4088,15 @@ export function REPL({
4073
4088
  onSubmitTask: handleIncomingPrompt
4074
4089
  });
4075
4090
 
4076
- // Loop mode: auto-tick when enabled (via /job command)
4091
+ }
4092
+
4093
+ // Proactive mode: auto-tick when enabled (/proactive, --proactive). Upstream
4094
+ // this sat inside the ant-only block above; the tick driver is needed in
4095
+ // every build where PROACTIVE is on.
4096
+ if (feature('PROACTIVE') || feature('KAIROS')) {
4077
4097
  // eslint-disable-next-line react-hooks/rules-of-hooks
4078
- // biome-ignore lint/correctness/useHookAtTopLevel: conditional for dead code elimination in external builds
4079
- useProactive?.({
4098
+ // biome-ignore lint/correctness/useHookAtTopLevel: feature() is a compile-time constant
4099
+ useProactive!({
4080
4100
  // Suppress ticks while an initial message is pending — the initial
4081
4101
  // message will be processed asynchronously and a premature tick would
4082
4102
  // race with it, causing concurrent-query enqueue of expanded skill text.
@@ -4584,7 +4604,7 @@ export function REPL({
4584
4604
  {"external" === 'ant' && <TungstenLiveMonitor />}
4585
4605
  {feature('WEB_BROWSER_TOOL') ? WebBrowserPanelModule && <WebBrowserPanelModule.WebBrowserPanel /> : null}
4586
4606
  <Box flexGrow={1} />
4587
- {showSpinner && <SpinnerWithVerb mode={streamMode} spinnerTip={spinnerTip} responseLengthRef={responseLengthRef} apiMetricsRef={apiMetricsRef} overrideMessage={spinnerMessage} spinnerSuffix={stopHookSpinnerSuffix} verbose={verbose} loadingStartTimeRef={loadingStartTimeRef} totalPausedMsRef={totalPausedMsRef} pauseStartTimeRef={pauseStartTimeRef} overrideColor={spinnerColor} overrideShimmerColor={spinnerShimmerColor} hasActiveTools={inProgressToolUseIDs.size > 0} leaderIsIdle={!isLoading} />}
4607
+ {showSpinner && <SpinnerWithVerb mode={streamMode} spinnerTip={spinnerTip} responseLengthRef={responseLengthRef} apiMetricsRef={apiMetricsRef} overrideMessage={spinnerMessage} compactProgress={compactProgress} spinnerSuffix={stopHookSpinnerSuffix} verbose={verbose} loadingStartTimeRef={loadingStartTimeRef} totalPausedMsRef={totalPausedMsRef} pauseStartTimeRef={pauseStartTimeRef} overrideColor={spinnerColor} overrideShimmerColor={spinnerShimmerColor} hasActiveTools={inProgressToolUseIDs.size > 0} leaderIsIdle={!isLoading} />}
4588
4608
  {!showSpinner && !isLoading && !userInputOnProcessing && !hasRunningTeammates && isBriefOnly && !viewedAgentTask && <BriefIdleStatus />}
4589
4609
  {isFullscreenEnvEnabled() && <PromptInputQueuedCommands />}
4590
4610
  </>} bottom={<Box flexDirection={feature('BUDDY') && companionNarrow ? 'column' : 'row'} width="100%" alignItems={feature('BUDDY') && companionNarrow ? undefined : 'flex-end'}>
@@ -1214,7 +1214,7 @@ async function* queryModel(
1214
1214
  cachedMCEnabled = featureEnabled && modelSupported
1215
1215
  const config = getCachedMCConfig()
1216
1216
  logForDebugging(
1217
- `Cached MC gate: enabled=${featureEnabled} modelSupported=${modelSupported} model=${options.model} supportedModels=${jsonStringify(config.supportedModels)}`,
1217
+ `Cached MC gate: enabled=${featureEnabled} modelSupported=${modelSupported} model=${options.model} supportedModels=${jsonStringify(config?.supportedModels)}`,
1218
1218
  )
1219
1219
  }
1220
1220
 
@@ -1951,7 +1951,9 @@ async function* queryModel(
1951
1951
  let totalStallTime = 0
1952
1952
  let stallCount = 0
1953
1953
 
1954
+ let __xelaParts = 0
1954
1955
  for await (const part of stream) {
1956
+ __xelaParts++
1955
1957
  resetStreamIdleTimer()
1956
1958
  const now = Date.now()
1957
1959
 
@@ -2316,6 +2318,9 @@ async function* queryModel(
2316
2318
  ...(part.type === 'message_start' ? { ttftMs } : undefined),
2317
2319
  }
2318
2320
  }
2321
+ if (process.env.XELA_LOADER_DEBUG) {
2322
+ process.stderr.write(`[claude.ts] stream loop exited: ${__xelaParts} parts, source=${options.querySource}, newMessages=${newMessages.length}\n`)
2323
+ }
2319
2324
  // Clear the idle timeout watchdog now that the stream loop has exited
2320
2325
  clearStreamIdleTimers()
2321
2326
 
@@ -31,11 +31,14 @@ const MAX_OUTPUT_TOKENS_FOR_SUMMARY = 20_000
31
31
 
32
32
  // Returns the context window size minus the max output tokens for the model
33
33
  export function getEffectiveContextWindowSize(model: string): number {
34
+ let contextWindow = getContextWindowForModel(model, getSdkBetas())
35
+ // The fixed reserves were sized for a 200k window. On a small local window
36
+ // (64k) they would leave almost nothing for the conversation, so scale them.
34
37
  const reservedTokensForSummary = Math.min(
35
38
  getMaxOutputTokensForModel(model),
36
39
  MAX_OUTPUT_TOKENS_FOR_SUMMARY,
40
+ smallWindowCap(contextWindow, 8),
37
41
  )
38
- let contextWindow = getContextWindowForModel(model, getSdkBetas())
39
42
 
40
43
  const autoCompactWindow = process.env.CLAUDE_CODE_AUTO_COMPACT_WINDOW
41
44
  if (autoCompactWindow) {
@@ -69,11 +72,20 @@ export const MANUAL_COMPACT_BUFFER_TOKENS = 3_000
69
72
  // in a single session, wasting ~250K API calls/day globally.
70
73
  const MAX_CONSECUTIVE_AUTOCOMPACT_FAILURES = 3
71
74
 
75
+ // OpenAI-compatible backends only: cap a fixed token reserve at 1/divisor of the
76
+ // window, so 64k-128k local models keep most of their context usable
77
+ function smallWindowCap(contextWindow: number, divisor: number): number {
78
+ return process.env.OPENAI_API_KEY
79
+ ? Math.floor(contextWindow / divisor)
80
+ : Number.POSITIVE_INFINITY
81
+ }
82
+
72
83
  export function getAutoCompactThreshold(model: string): number {
73
84
  const effectiveContextWindow = getEffectiveContextWindowSize(model)
74
85
 
75
86
  const autocompactThreshold =
76
- effectiveContextWindow - AUTOCOMPACT_BUFFER_TOKENS
87
+ effectiveContextWindow -
88
+ Math.min(AUTOCOMPACT_BUFFER_TOKENS, smallWindowCap(effectiveContextWindow, 10))
77
89
 
78
90
  // Override for easier testing of autocompact
79
91
  const envPercent = process.env.CLAUDE_AUTOCOMPACT_PCT_OVERRIDE
@@ -110,8 +122,10 @@ export function calculateTokenWarningState(
110
122
  Math.round(((threshold - tokenUsage) / threshold) * 100),
111
123
  )
112
124
 
113
- const warningThreshold = threshold - WARNING_THRESHOLD_BUFFER_TOKENS
114
- const errorThreshold = threshold - ERROR_THRESHOLD_BUFFER_TOKENS
125
+ const warningThreshold =
126
+ threshold - Math.min(WARNING_THRESHOLD_BUFFER_TOKENS, smallWindowCap(threshold, 8))
127
+ const errorThreshold =
128
+ threshold - Math.min(ERROR_THRESHOLD_BUFFER_TOKENS, smallWindowCap(threshold, 8))
115
129
 
116
130
  const isAboveWarningThreshold = tokenUsage >= warningThreshold
117
131
  const isAboveErrorThreshold = tokenUsage >= errorThreshold
@@ -0,0 +1 @@
1
+ export { getCachedMCConfig, type CachedMCConfig } from './cachedMicrocompact.js'
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Cached microcompact: instead of rewriting old tool results locally, ask the
3
+ * API to drop them from the cached prompt with `cache_edits` blocks. That
4
+ * needs Anthropic's cache-editing beta, which no OpenAI-compatible backend
5
+ * offers, so this implementation keeps the bookkeeping but never enables the
6
+ * path. Enabling it requires CLAUDE_CODE_CACHED_MICROCOMPACT=1 and a model
7
+ * listed in the config.
8
+ */
9
+ import { isEnvTruthy } from '../../utils/envUtils.js'
10
+
11
+ export type CacheEdit = {
12
+ type: 'clear_tool_uses_20250919'
13
+ tool_use_ids: string[]
14
+ }
15
+
16
+ export type CacheEditsBlock = {
17
+ type: 'cache_edits'
18
+ edits: CacheEdit[]
19
+ }
20
+
21
+ export type PinnedCacheEdits = {
22
+ userMessageIndex: number
23
+ block: CacheEditsBlock
24
+ }
25
+
26
+ export type CachedMCState = {
27
+ /** tool_use_id -> whether the result has been sent to the API at least once */
28
+ toolResults: Map<string, { sent: boolean; order: number }>
29
+ /** tool_use_ids grouped by the user message that carried them, oldest first */
30
+ toolMessages: string[][]
31
+ /** ids already deleted via cache edits */
32
+ deleted: Set<string>
33
+ pinnedEdits: PinnedCacheEdits[]
34
+ nextOrder: number
35
+ }
36
+
37
+ export type CachedMCConfig = {
38
+ supportedModels: string[]
39
+ /** start deleting once this many sent tool results exist */
40
+ triggerThreshold: number
41
+ /** keep this many most recent tool results */
42
+ keepRecent: number
43
+ }
44
+
45
+ const DEFAULT_CONFIG: CachedMCConfig = {
46
+ supportedModels: [],
47
+ triggerThreshold: 40,
48
+ keepRecent: 20,
49
+ }
50
+
51
+ export function getCachedMCConfig(): CachedMCConfig {
52
+ const models = (process.env.CLAUDE_CODE_CACHED_MC_MODELS || '')
53
+ .split(',')
54
+ .map(s => s.trim())
55
+ .filter(Boolean)
56
+ return { ...DEFAULT_CONFIG, supportedModels: models }
57
+ }
58
+
59
+ export function isCachedMicrocompactEnabled(): boolean {
60
+ return isEnvTruthy(process.env.CLAUDE_CODE_CACHED_MICROCOMPACT)
61
+ }
62
+
63
+ export function isModelSupportedForCacheEditing(model: string): boolean {
64
+ if (!isCachedMicrocompactEnabled()) return false
65
+ return getCachedMCConfig().supportedModels.some(m => model.includes(m))
66
+ }
67
+
68
+ export function createCachedMCState(): CachedMCState {
69
+ return {
70
+ toolResults: new Map(),
71
+ toolMessages: [],
72
+ deleted: new Set(),
73
+ pinnedEdits: [],
74
+ nextOrder: 0,
75
+ }
76
+ }
77
+
78
+ export function resetCachedMCState(state: CachedMCState): void {
79
+ state.toolResults.clear()
80
+ state.toolMessages = []
81
+ state.deleted.clear()
82
+ state.pinnedEdits = []
83
+ state.nextOrder = 0
84
+ }
85
+
86
+ export function registerToolResult(state: CachedMCState, toolUseId: string): void {
87
+ if (!state.toolResults.has(toolUseId)) {
88
+ state.toolResults.set(toolUseId, { sent: false, order: state.nextOrder++ })
89
+ }
90
+ }
91
+
92
+ export function registerToolMessage(state: CachedMCState, toolUseIds: string[]): void {
93
+ if (toolUseIds.length > 0) state.toolMessages.push([...toolUseIds])
94
+ }
95
+
96
+ export function markToolsSentToAPI(state: CachedMCState): void {
97
+ for (const entry of state.toolResults.values()) entry.sent = true
98
+ }
99
+
100
+ /** Oldest sent tool results beyond the keep window, not yet deleted. */
101
+ export function getToolResultsToDelete(state: CachedMCState): string[] {
102
+ const { triggerThreshold, keepRecent } = getCachedMCConfig()
103
+ const sent = [...state.toolResults.entries()]
104
+ .filter(([id, e]) => e.sent && !state.deleted.has(id))
105
+ .sort((a, b) => a[1].order - b[1].order)
106
+ if (sent.length < triggerThreshold) return []
107
+ return sent.slice(0, Math.max(0, sent.length - keepRecent)).map(([id]) => id)
108
+ }
109
+
110
+ export function createCacheEditsBlock(state: CachedMCState, toolUseIds: string[]): CacheEditsBlock {
111
+ for (const id of toolUseIds) state.deleted.add(id)
112
+ return { type: 'cache_edits', edits: [{ type: 'clear_tool_uses_20250919', tool_use_ids: toolUseIds }] }
113
+ }
@@ -426,7 +426,7 @@ export async function compactConversation(
426
426
  // Show requesting mode with up arrow and custom message
427
427
  context.setStreamMode?.('requesting')
428
428
  context.setResponseLength?.(() => 0)
429
- context.onCompactProgress?.({ type: 'compact_start' })
429
+ context.onCompactProgress?.({ type: 'compact_start', preCompactTokenCount })
430
430
 
431
431
  // 3P default: true — forked-agent path reuses main conversation's prompt cache.
432
432
  // Experiment (Jan 2026) confirmed: false path is 98% cache miss, costs ~0.76% of
@@ -835,7 +835,7 @@ export async function partialCompactConversation(
835
835
 
836
836
  context.setStreamMode?.('requesting')
837
837
  context.setResponseLength?.(() => 0)
838
- context.onCompactProgress?.({ type: 'compact_start' })
838
+ context.onCompactProgress?.({ type: 'compact_start', preCompactTokenCount })
839
839
 
840
840
  const compactPrompt = getPartialCompactPrompt(customInstructions, direction)
841
841
  const summaryRequest = createUserMessage({
@@ -1197,6 +1197,21 @@ async function streamCompactSummary({
1197
1197
  // fork — same signal the streaming fallback uses at
1198
1198
  // `signal: context.abortController.signal` below.
1199
1199
  overrides: { abortController: context.abortController },
1200
+ // Feed streamed summary text into the parent's response counter so
1201
+ // the spinner's progress bar and token count move while the
1202
+ // summary is generated (the fork's own context is a no-op there).
1203
+ onStreamEvent: message => {
1204
+ const ev = message as { type?: string; event?: { type?: string; delta?: { type?: string; text?: string } } }
1205
+ if (
1206
+ ev.type === 'stream_event' &&
1207
+ ev.event?.type === 'content_block_delta' &&
1208
+ ev.event.delta?.type === 'text_delta' &&
1209
+ typeof ev.event.delta.text === 'string'
1210
+ ) {
1211
+ const n = ev.event.delta.text.length
1212
+ context.setResponseLength?.(length => length + n)
1213
+ }
1214
+ },
1200
1215
  })
1201
1216
  const assistantMsg = getLastAssistantMessage(result.messages)
1202
1217
  const assistantText = assistantMsg
@@ -1325,6 +1340,7 @@ async function streamCompactSummary({
1325
1340
  },
1326
1341
  })
1327
1342
  const streamIter = streamingGen[Symbol.asyncIterator]()
1343
+ let summaryCharsStreamed = 0
1328
1344
  let next = await streamIter.next()
1329
1345
 
1330
1346
  while (!next.done) {
@@ -1346,6 +1362,7 @@ async function streamCompactSummary({
1346
1362
  event.event.delta.type === 'text_delta'
1347
1363
  ) {
1348
1364
  const charactersStreamed = event.event.delta.text.length
1365
+ summaryCharsStreamed += charactersStreamed
1349
1366
  context.setResponseLength?.(length => length + charactersStreamed)
1350
1367
  }
1351
1368
 
@@ -1357,6 +1374,9 @@ async function streamCompactSummary({
1357
1374
  }
1358
1375
 
1359
1376
  if (response) {
1377
+ logForDebugging(
1378
+ `[compact] summary streamed: ${summaryCharsStreamed} chars, hasStartedStreaming=${hasStartedStreaming}`,
1379
+ )
1360
1380
  return response
1361
1381
  }
1362
1382