@vybestack/llxprt-code-core 0.10.0 → 0.11.0-nightly.260728.ffaa8d5d7

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 (168) hide show
  1. package/dist/src/config/config.d.ts +20 -0
  2. package/dist/src/config/config.js +143 -23
  3. package/dist/src/config/config.js.map +1 -1
  4. package/dist/src/config/configBaseCore.d.ts +6 -2
  5. package/dist/src/config/configBaseCore.js +8 -0
  6. package/dist/src/config/configBaseCore.js.map +1 -1
  7. package/dist/src/config/configConstructor.d.ts +2 -0
  8. package/dist/src/config/configConstructor.js +9 -1
  9. package/dist/src/config/configConstructor.js.map +1 -1
  10. package/dist/src/config/configTestHarness.d.ts +8 -6
  11. package/dist/src/config/configTestHarness.js +10 -4
  12. package/dist/src/config/configTestHarness.js.map +1 -1
  13. package/dist/src/config/configTypes.d.ts +7 -2
  14. package/dist/src/config/configTypes.js +2 -0
  15. package/dist/src/config/configTypes.js.map +1 -1
  16. package/dist/src/config/liveTrustTransitionLifecycle.d.ts +35 -0
  17. package/dist/src/config/liveTrustTransitionLifecycle.js +180 -0
  18. package/dist/src/config/liveTrustTransitionLifecycle.js.map +1 -0
  19. package/dist/src/config/schedulerSingleton.d.ts +2 -2
  20. package/dist/src/config/schedulerSingleton.js.map +1 -1
  21. package/dist/src/core/compression/types.d.ts +48 -1
  22. package/dist/src/core/compression/types.js +41 -0
  23. package/dist/src/core/compression/types.js.map +1 -1
  24. package/dist/src/core/model-limits.json +96 -0
  25. package/dist/src/core/model-limits.schema.d.ts +123 -0
  26. package/dist/src/core/model-limits.schema.js +75 -0
  27. package/dist/src/core/model-limits.schema.js.map +1 -0
  28. package/dist/src/core/tokenLimits.d.ts +6 -8
  29. package/dist/src/core/tokenLimits.js +56 -132
  30. package/dist/src/core/tokenLimits.js.map +1 -1
  31. package/dist/src/core/turn.d.ts +20 -3
  32. package/dist/src/core/turn.js +20 -3
  33. package/dist/src/core/turn.js.map +1 -1
  34. package/dist/src/hooks/hookRegistry.d.ts +1 -1
  35. package/dist/src/hooks/hookRegistry.js +3 -1
  36. package/dist/src/hooks/hookRegistry.js.map +1 -1
  37. package/dist/src/hooks/hookRunner.d.ts +6 -0
  38. package/dist/src/hooks/hookRunner.js +22 -6
  39. package/dist/src/hooks/hookRunner.js.map +1 -1
  40. package/dist/src/hooks/hookSystem.d.ts +10 -1
  41. package/dist/src/hooks/hookSystem.js +69 -12
  42. package/dist/src/hooks/hookSystem.js.map +1 -1
  43. package/dist/src/index.d.ts +1 -0
  44. package/dist/src/index.js +1 -0
  45. package/dist/src/index.js.map +1 -1
  46. package/dist/src/llm-types/geminiContent.d.ts +3 -0
  47. package/dist/src/policy/config.d.ts +4 -2
  48. package/dist/src/policy/config.js +22 -18
  49. package/dist/src/policy/config.js.map +1 -1
  50. package/dist/src/recording/ReplayEngine.js +39 -2
  51. package/dist/src/recording/ReplayEngine.js.map +1 -1
  52. package/dist/src/recording/SessionDiscovery.d.ts +8 -0
  53. package/dist/src/recording/SessionDiscovery.js +88 -3
  54. package/dist/src/recording/SessionDiscovery.js.map +1 -1
  55. package/dist/src/recording/SessionLockManager.d.ts +3 -0
  56. package/dist/src/recording/SessionLockManager.js +9 -3
  57. package/dist/src/recording/SessionLockManager.js.map +1 -1
  58. package/dist/src/recording/SessionRecordingService.d.ts +14 -1
  59. package/dist/src/recording/SessionRecordingService.js +27 -3
  60. package/dist/src/recording/SessionRecordingService.js.map +1 -1
  61. package/dist/src/recording/index.d.ts +4 -3
  62. package/dist/src/recording/index.js +4 -3
  63. package/dist/src/recording/index.js.map +1 -1
  64. package/dist/src/recording/replay-test-helpers.d.ts +5 -0
  65. package/dist/src/recording/replay-test-helpers.js +13 -0
  66. package/dist/src/recording/replay-test-helpers.js.map +1 -1
  67. package/dist/src/recording/resumeNotFoundMessages.d.ts +56 -0
  68. package/dist/src/recording/resumeNotFoundMessages.js +64 -0
  69. package/dist/src/recording/resumeNotFoundMessages.js.map +1 -0
  70. package/dist/src/recording/resumeSession.js +3 -2
  71. package/dist/src/recording/resumeSession.js.map +1 -1
  72. package/dist/src/recording/sessionManagement.d.ts +3 -0
  73. package/dist/src/recording/sessionManagement.js +38 -19
  74. package/dist/src/recording/sessionManagement.js.map +1 -1
  75. package/dist/src/recording/types.d.ts +42 -3
  76. package/dist/src/recording/types.js +1 -1
  77. package/dist/src/recording/types.js.map +1 -1
  78. package/dist/src/runtime/AgentRuntimeLoader.js +2 -1
  79. package/dist/src/runtime/AgentRuntimeLoader.js.map +1 -1
  80. package/dist/src/scheduler/liveOutput.d.ts +10 -19
  81. package/dist/src/scheduler/liveOutput.js +21 -21
  82. package/dist/src/scheduler/liveOutput.js.map +1 -1
  83. package/dist/src/scheduler/types.d.ts +2 -2
  84. package/dist/src/services/history/ContentConverters.d.ts +4 -0
  85. package/dist/src/services/history/ContentConverters.js +77 -59
  86. package/dist/src/services/history/ContentConverters.js.map +1 -1
  87. package/dist/src/services/history/HistoryService.d.ts +13 -1
  88. package/dist/src/services/history/HistoryService.js +71 -0
  89. package/dist/src/services/history/HistoryService.js.map +1 -1
  90. package/dist/src/services/history/IContent.d.ts +10 -0
  91. package/dist/src/services/history/IContent.js.map +1 -1
  92. package/dist/src/skills/skillManager.d.ts +11 -1
  93. package/dist/src/skills/skillManager.js +21 -1
  94. package/dist/src/skills/skillManager.js.map +1 -1
  95. package/dist/src/test-utils/mock-tool.d.ts +3 -3
  96. package/dist/src/test-utils/mock-tool.js.map +1 -1
  97. package/dist/src/test-utils/tools.d.ts +2 -2
  98. package/dist/src/test-utils/tools.js.map +1 -1
  99. package/dist/src/tools-adapters/CoreShellToolHostAdapter.js +1 -6
  100. package/dist/src/tools-adapters/CoreShellToolHostAdapter.js.map +1 -1
  101. package/dist/src/utils/browser-profile-discovery.d.ts +40 -0
  102. package/dist/src/utils/browser-profile-discovery.js +251 -0
  103. package/dist/src/utils/browser-profile-discovery.js.map +1 -0
  104. package/dist/src/utils/env.d.ts +12 -0
  105. package/dist/src/utils/env.js +15 -0
  106. package/dist/src/utils/env.js.map +1 -0
  107. package/dist/src/utils/events.d.ts +10 -1
  108. package/dist/src/utils/events.js +22 -0
  109. package/dist/src/utils/events.js.map +1 -1
  110. package/dist/src/utils/fileUtils.js +8 -1
  111. package/dist/src/utils/fileUtils.js.map +1 -1
  112. package/dist/src/utils/retry.js +3 -4
  113. package/dist/src/utils/retry.js.map +1 -1
  114. package/dist/src/utils/secure-browser-launcher.d.ts +35 -1
  115. package/dist/src/utils/secure-browser-launcher.js +341 -1
  116. package/dist/src/utils/secure-browser-launcher.js.map +1 -1
  117. package/dist/src/utils/terminalSerializer.d.ts +12 -0
  118. package/dist/src/utils/terminalSerializer.js.map +1 -1
  119. package/dist/src/utils/thoughtUtils.d.ts +2 -0
  120. package/dist/src/utils/thoughtUtils.js.map +1 -1
  121. package/package.json +13 -9
  122. package/src/config/config.ts +161 -22
  123. package/src/config/configBaseCore.ts +10 -2
  124. package/src/config/configConstructor.ts +11 -0
  125. package/src/config/configTestHarness.ts +10 -4
  126. package/src/config/configTypes.ts +6 -1
  127. package/src/config/liveTrustTransitionLifecycle.ts +250 -0
  128. package/src/config/schedulerSingleton.ts +2 -5
  129. package/src/core/compression/types.ts +81 -1
  130. package/src/core/legacy-model-limits.expected.txt +96 -0
  131. package/src/core/model-limits.json +96 -0
  132. package/src/core/model-limits.schema.ts +87 -0
  133. package/src/core/tokenLimits.ts +73 -147
  134. package/src/core/turn.ts +20 -3
  135. package/src/hooks/hookRegistry.ts +3 -1
  136. package/src/hooks/hookRunner.ts +24 -7
  137. package/src/hooks/hookSystem.ts +73 -13
  138. package/src/index.ts +1 -0
  139. package/src/llm-types/geminiContent.ts +3 -0
  140. package/src/policy/config.ts +32 -19
  141. package/src/recording/ReplayEngine.ts +51 -2
  142. package/src/recording/SessionDiscovery.ts +99 -4
  143. package/src/recording/SessionLockManager.ts +10 -3
  144. package/src/recording/SessionRecordingService.ts +36 -3
  145. package/src/recording/index.ts +19 -2
  146. package/src/recording/replay-test-helpers.ts +14 -0
  147. package/src/recording/resumeNotFoundMessages.ts +75 -0
  148. package/src/recording/resumeSession.ts +3 -1
  149. package/src/recording/sessionManagement.ts +51 -21
  150. package/src/recording/types.ts +44 -2
  151. package/src/runtime/AgentRuntimeLoader.ts +3 -1
  152. package/src/scheduler/liveOutput.ts +25 -22
  153. package/src/scheduler/types.ts +5 -2
  154. package/src/services/history/ContentConverters.ts +91 -68
  155. package/src/services/history/HistoryService.ts +92 -1
  156. package/src/services/history/IContent.ts +13 -0
  157. package/src/skills/skillManager.ts +26 -1
  158. package/src/test-utils/mock-tool.ts +4 -3
  159. package/src/test-utils/tools.ts +4 -3
  160. package/src/tools-adapters/CoreShellToolHostAdapter.ts +1 -7
  161. package/src/utils/browser-profile-discovery.ts +358 -0
  162. package/src/utils/env.ts +15 -0
  163. package/src/utils/events.ts +31 -0
  164. package/src/utils/fileUtils.ts +11 -1
  165. package/src/utils/retry.ts +3 -4
  166. package/src/utils/secure-browser-launcher.ts +500 -2
  167. package/src/utils/terminalSerializer.ts +9 -0
  168. package/src/utils/thoughtUtils.ts +2 -0
@@ -4,123 +4,84 @@
4
4
  * SPDX-License-Identifier: Apache-2.0
5
5
  */
6
6
 
7
+ import { ModelLimitsCatalogSchema } from './model-limits.schema.js';
8
+ import type { ModelLimitsCatalog } from './model-limits.schema.js';
9
+ import catalogData from './model-limits.json' with { type: 'json' };
10
+
7
11
  type Model = string;
8
12
  type TokenCount = number;
9
13
 
10
- /**
11
- * Conservative fallback for unrecognized models.
12
- *
13
- * Lowered from 1M to 200K (issue #2270 / #2527): the overwhelmingly common
14
- * real-world floor is 200K (Claude, many GLM/Gemini variants). Defaulting to
15
- * 200K instead of 1M makes the unrecognized-model case fail-safe — proactive
16
- * compression fires conservatively rather than silently overrunning a
17
- * sub-200K backend.
18
- */
19
- export const DEFAULT_TOKEN_LIMIT = 200_000;
20
-
21
- const OPENAI_128K_PREFIXES = [
22
- 'o4-mini',
23
- 'gpt-4o-mini',
24
- 'gpt-4o-realtime',
25
- 'gpt-4o',
26
- 'gpt-4-turbo',
27
- ];
14
+ // Catalog corruption is a release/configuration error. Validate eagerly so an
15
+ // invalid package fails deterministically instead of silently using stale limits.
16
+ const CATALOG = ModelLimitsCatalogSchema.parse(catalogData);
28
17
 
29
- const OPENAI_200K_PREFIXES = ['o3-pro', 'o3-mini', 'o1-mini', 'o3', 'o1'];
18
+ export const DEFAULT_TOKEN_LIMIT: TokenCount = CATALOG.defaultLimit;
30
19
 
31
- interface PrefixLimit {
32
- prefix: string;
33
- limit: TokenCount;
20
+ function matchesAnyPrefix(model: string, prefixes: readonly string[]): boolean {
21
+ return prefixes.some((prefix) => model.startsWith(prefix));
34
22
  }
35
23
 
36
- const PREFIX_LIMITS: PrefixLimit[] = [
37
- { prefix: 'gpt-4.1', limit: 1_000_000 },
38
- { prefix: 'gpt-3.5-turbo', limit: 16_385 },
39
- // Gemini 2.x variants (experimental, preview, dated snapshots) — all 1M.
40
- // Exact entries above take precedence, so the 32K image-generation variant
41
- // is unaffected. Prevents the lowered DEFAULT_TOKEN_LIMIT from silently
42
- // shrinking recognized Gemini variants (issue #2527).
43
- { prefix: 'gemini-2.5-pro', limit: 1_048_576 },
44
- { prefix: 'gemini-2.5-flash', limit: 1_048_576 },
45
- { prefix: 'gemini-2.0-flash', limit: 1_048_576 },
46
- // GLM-5 family — 200K context window (z.ai API docs)
47
- { prefix: 'glm-5', limit: 200_000 },
48
- // GLM-4 family — 128K context window (z.ai API docs)
49
- { prefix: 'glm-4', limit: 128_000 },
50
- ];
51
-
52
- const EXACT_LIMITS: Record<string, TokenCount> = {
53
- // GLM (z.ai) models — exact entries for commonly used variants.
54
- // The GLM-5 family (glm-5, glm-5.1, glm-5.2, …) exposes a 200K context
55
- // window; the GLM-4 family (glm-4, glm-4.5, glm-4.6, …) exposes 128K.
56
- // Prefix-based fallbacks below cover future/dated variants.
57
- 'glm-5.2': 200_000,
58
- 'glm-5.1': 200_000,
59
- 'glm-5': 200_000,
60
- 'glm-4.6': 128_000,
61
- 'glm-4.5': 128_000,
62
- 'glm-4': 128_000,
63
-
64
- // Gemini models
65
- 'gemini-1.5-pro': 2_097_152,
66
- 'gemini-1.5-flash': 1_048_576,
67
- 'gemini-2.5-pro-preview-05-06': 1_048_576,
68
- 'gemini-2.5-pro-preview-06-05': 1_048_576,
69
- 'gemini-2.5-pro': 1_048_576,
70
- 'gemini-2.5-flash-preview-05-20': 1_048_576,
71
- 'gemini-2.5-flash': 1_048_576,
72
- 'gemini-2.5-flash-lite': 1_048_576,
73
- 'gemini-2.0-flash': 1_048_576,
74
- 'gemini-2.0-flash-preview-image-generation': 32_000,
75
-
76
- // Claude Opus 5 and Opus 4.6/4.7/4.8 default to the Claude Code /
77
- // subscription 200K context window. The 1M window is API-only and
78
- // plan-gated; override via /set or a profile (context-limit).
79
- 'claude-opus-5': 200_000,
80
- 'claude-opus-5-latest': 200_000,
81
- 'claude-opus-4-8': 200_000,
82
- 'claude-opus-4-7': 200_000,
83
- 'claude-opus-4-6': 200_000,
84
- 'claude-opus-4-latest': 200_000,
85
- 'claude-sonnet-4-6': 200_000,
86
- 'claude-sonnet-4-latest': 400_000,
87
- // Claude Sonnet 5 defaults to the Claude Code / subscription 200K context
88
- // window. The advertised 1M window is API-only and plan-gated; override via
89
- // /set or a profile (context-limit).
90
- 'claude-sonnet-5': 200_000,
91
- 'claude-sonnet-5-latest': 200_000,
92
- // Claude Fable 5 defaults to the Claude Code / subscription 200K context
93
- // window. The advertised 1M window is API-only and plan-gated; override via
94
- // /set or a profile (context-limit). Mirrors getContextWindowForModel.
95
- 'claude-fable-5': 200_000,
96
- 'claude-fable-5-latest': 200_000,
97
- 'claude-3-7-opus-20250115': 300_000,
98
- 'claude-3-7-sonnet-20250115': 300_000,
99
- 'claude-3-opus-20240229': 200_000,
100
- 'claude-3-sonnet-20240229': 200_000,
101
- 'claude-3-haiku-20240307': 200_000,
102
- 'claude-3.5-sonnet-20240620': 200_000,
103
- 'claude-3.5-sonnet-20241022': 200_000,
104
- 'claude-3.5-haiku-20241022': 200_000,
105
- 'claude-3-5-sonnet-20241022': 200_000,
106
- 'claude-3-5-haiku-20241022': 200_000,
107
- };
108
-
109
- function matchesAnyPrefix(model: string, prefixes: string[]): boolean {
110
- return prefixes.some((prefix) => model.startsWith(prefix));
24
+ function assertUnreachableRule(rule: never): never {
25
+ throw new Error(`Unsupported model-limit rule: ${JSON.stringify(rule)}`);
111
26
  }
112
27
 
113
- function resolvePrefixLimit(
28
+ /**
29
+ * Resolve a single catalog's ordered rules against a model id + provider
30
+ * prefix. Exported so that the case-insensitive normalization can be tested
31
+ * with any catalog (including mixed-case fixtures) without type assertions.
32
+ */
33
+ export function resolveOrderedRuleFromCatalog(
34
+ catalog: ModelLimitsCatalog,
114
35
  modelWithoutPrefix: string,
36
+ providerPrefix: string,
115
37
  ): TokenCount | undefined {
116
- for (const { prefix, limit } of PREFIX_LIMITS) {
117
- if (modelWithoutPrefix.startsWith(prefix)) {
118
- return limit;
38
+ for (const rule of catalog.orderedRules) {
39
+ switch (rule.type) {
40
+ case 'substring':
41
+ if (modelWithoutPrefix.includes(rule.substring)) {
42
+ return rule.limit;
43
+ }
44
+ break;
45
+ case 'substringOrProviderPrefix':
46
+ if (
47
+ modelWithoutPrefix.includes(rule.substring) ||
48
+ providerPrefix === rule.providerPrefix
49
+ ) {
50
+ return rule.limit;
51
+ }
52
+ break;
53
+ case 'prefixGroup':
54
+ if (matchesAnyPrefix(modelWithoutPrefix, rule.prefixes)) {
55
+ return rule.limit;
56
+ }
57
+ break;
58
+ case 'substringCaseInsensitive':
59
+ if (
60
+ modelWithoutPrefix
61
+ .toLowerCase()
62
+ .includes(rule.substring.toLowerCase())
63
+ ) {
64
+ return rule.limit;
65
+ }
66
+ break;
67
+ default:
68
+ return assertUnreachableRule(rule);
119
69
  }
120
70
  }
121
71
  return undefined;
122
72
  }
123
73
 
74
+ function resolveOrderedRule(
75
+ modelWithoutPrefix: string,
76
+ providerPrefix: string,
77
+ ): TokenCount | undefined {
78
+ return resolveOrderedRuleFromCatalog(
79
+ CATALOG,
80
+ modelWithoutPrefix,
81
+ providerPrefix,
82
+ );
83
+ }
84
+
124
85
  export function tokenLimit(
125
86
  model: Model,
126
87
  userContextLimit?: number,
@@ -137,57 +98,22 @@ export function tokenLimit(
137
98
  colonIndex !== -1 ? model.slice(colonIndex + 1) : model;
138
99
 
139
100
  // Check exact model matches first
140
- if (modelWithoutPrefix in EXACT_LIMITS) {
141
- return EXACT_LIMITS[modelWithoutPrefix];
101
+ const exactLimit = CATALOG.exactLimits[modelWithoutPrefix];
102
+ if (typeof exactLimit === 'number') {
103
+ return exactLimit;
142
104
  }
143
105
 
144
106
  // Check prefix-based limits
145
- const prefixLimit = resolvePrefixLimit(modelWithoutPrefix);
146
- if (prefixLimit !== undefined) {
147
- return prefixLimit;
148
- }
149
-
150
- // Codex models default to 256K context (per composition/aliases/codex.config).
151
- // variant has a smaller 128K window and must be checked first since its ID
152
- // also contains the substring "codex". Non-suffixed codex model IDs
153
- // (gpt-5.5, gpt-5.4, gpt-5.2, gpt-5.1) are only resolvable when the
154
- // provider prefix is explicitly "codex" — a bare ID is ambiguous and could
155
- // belong to the regular OpenAI provider.
156
- if (modelWithoutPrefix.includes('codex-spark')) {
157
- return 131_072;
158
- }
159
- if (modelWithoutPrefix.includes('codex') || providerPrefix === 'codex') {
160
- return 262_144;
161
- }
162
-
163
- // Check OpenAI 200K models (includes o3, o1 series)
164
- if (matchesAnyPrefix(modelWithoutPrefix, OPENAI_200K_PREFIXES)) {
165
- return 200_000;
166
- }
167
-
168
- // Check OpenAI 128K models
169
- if (matchesAnyPrefix(modelWithoutPrefix, OPENAI_128K_PREFIXES)) {
170
- return 128_000;
171
- }
172
-
173
- // Claude Sonnet 5 dated snapshot variants (e.g. claude-sonnet-5-YYYYMMDD)
174
- // are not individually exact-keyed above; resolve them to the same
175
- // subscription-safe 200K default as the bare alias and -latest. Mirrors
176
- // getContextWindowForModel in the anthropic package.
177
- if (modelWithoutPrefix.toLowerCase().includes('claude-sonnet-5')) {
178
- return 200_000;
179
- }
180
-
181
- // Claude Opus 5 dated snapshot variants (e.g. claude-opus-5-YYYYMMDD)
182
- // resolve to the same subscription-safe 200K default.
183
- if (modelWithoutPrefix.toLowerCase().includes('claude-opus-5')) {
184
- return 200_000;
107
+ for (const { prefix, limit } of CATALOG.prefixLimits) {
108
+ if (modelWithoutPrefix.startsWith(prefix)) {
109
+ return limit;
110
+ }
185
111
  }
186
112
 
187
- // Claude Fable 5 dated snapshot variants (e.g. claude-fable-5-YYYYMMDD)
188
- // resolve to the same subscription-safe 200K default.
189
- if (modelWithoutPrefix.toLowerCase().includes('claude-fable-5')) {
190
- return 200_000;
113
+ // Check ordered data-driven rules (codex, OpenAI groups, Claude substrings)
114
+ const orderedLimit = resolveOrderedRule(modelWithoutPrefix, providerPrefix);
115
+ if (orderedLimit !== undefined) {
116
+ return orderedLimit;
191
117
  }
192
118
 
193
119
  return DEFAULT_TOKEN_LIMIT;
package/src/core/turn.ts CHANGED
@@ -199,10 +199,19 @@ export enum CompressionStatus {
199
199
  /** The compression failed due to an error counting tokens */
200
200
  COMPRESSION_FAILED_TOKEN_COUNT_ERROR = 3,
201
201
 
202
- /** The compression failed because the model returned an empty summary */
203
- COMPRESSION_FAILED_EMPTY_SUMMARY,
202
+ /**
203
+ * The compression failed because the model returned an empty summary.
204
+ * Explicit value to avoid colliding with NOOP (issue #2602).
205
+ */
206
+ COMPRESSION_FAILED_EMPTY_SUMMARY = 7,
204
207
 
205
- /** The compression was not necessary and no action was taken */
208
+ /**
209
+ * Structural compression was a no-op: the strategy determined it could not
210
+ * produce a valid compressed candidate (e.g. no valid middle after split /
211
+ * tool-boundary / last-user-prompt preservation) and returned the history
212
+ * unchanged. Distinct serialized value from COMPRESSION_FAILED_EMPTY_SUMMARY
213
+ * (issue #2602).
214
+ */
206
215
  NOOP = 4,
207
216
 
208
217
  /** Compression ran recently and did not reduce tokens further */
@@ -224,6 +233,14 @@ export enum PerformCompressionResult {
224
233
  SKIPPED_EMPTY = 'skipped_empty',
225
234
  /** Compression skipped due to cooldown after repeated failures */
226
235
  SKIPPED_COOLDOWN = 'skipped_cooldown',
236
+ /**
237
+ * Structural compression was a no-op: the configured strategy (and, for
238
+ * middle-out, the one-shot fallback) determined it could not produce a valid
239
+ * compressed candidate and returned history unchanged. Distinct from skips
240
+ * (which never attempted) and from FAILED (which attempted and threw).
241
+ * (issue #2602)
242
+ */
243
+ NOOP = 'noop',
227
244
  /** Compression was attempted but all strategies failed */
228
245
  FAILED = 'failed',
229
246
  }
@@ -55,9 +55,11 @@ export class HookRegistry {
55
55
  /**
56
56
  * Initialize the registry by processing hooks from config
57
57
  */
58
- async initialize(): Promise<void> {
58
+ async initialize(signal?: AbortSignal): Promise<void> {
59
+ signal?.throwIfAborted();
59
60
  this.entries = [];
60
61
  this.processHooksFromConfig();
62
+ signal?.throwIfAborted();
61
63
 
62
64
  debugLogger.log(
63
65
  `Hook registry initialized with ${this.entries.length} hook entries`,
@@ -299,7 +299,7 @@ export class HookRunner {
299
299
 
300
300
  const child = this.spawnHookProcess(hookConfig, input);
301
301
 
302
- const timeoutHandle = this.setupKillTimeout(child, timeout, () => {
302
+ const timeoutControl = this.setupKillTimeout(child, timeout, () => {
303
303
  timedOut = true;
304
304
  });
305
305
 
@@ -314,7 +314,7 @@ export class HookRunner {
314
314
  });
315
315
 
316
316
  child.on('close', (exitCode) => {
317
- clearTimeout(timeoutHandle);
317
+ timeoutControl.clear();
318
318
  const duration = Date.now() - startTime;
319
319
  if (timedOut) {
320
320
  resolve(
@@ -342,7 +342,7 @@ export class HookRunner {
342
342
  });
343
343
 
344
344
  child.on('error', (error) => {
345
- clearTimeout(timeoutHandle);
345
+ timeoutControl.clear();
346
346
  resolve(
347
347
  this.errorResult(
348
348
  hookConfig,
@@ -373,20 +373,37 @@ export class HookRunner {
373
373
  };
374
374
  }
375
375
 
376
+ /**
377
+ * Sends SIGTERM after `timeout`, then escalates to SIGKILL if the child is
378
+ * still running. Do not use `child.killed` for escalation — Node sets that
379
+ * when a signal is *sent*, not when the process exits, so SIGTERM-ignoring
380
+ * hooks would never receive SIGKILL and could hang forever.
381
+ */
376
382
  private setupKillTimeout(
377
383
  child: ReturnType<typeof spawn>,
378
384
  timeout: number,
379
385
  onTimeout: () => void,
380
- ): NodeJS.Timeout {
381
- return setTimeout(() => {
386
+ ): { clear: () => void } {
387
+ let forceKillHandle: NodeJS.Timeout | undefined;
388
+ const timeoutHandle = setTimeout(() => {
382
389
  onTimeout();
383
390
  child.kill('SIGTERM');
384
- setTimeout(() => {
385
- if (!child.killed) {
391
+ forceKillHandle = setTimeout(() => {
392
+ // Still running only when both exit indicators remain null.
393
+ if (child.exitCode === null && child.signalCode === null) {
386
394
  child.kill('SIGKILL');
387
395
  }
388
396
  }, 5000);
389
397
  }, timeout);
398
+
399
+ return {
400
+ clear: () => {
401
+ clearTimeout(timeoutHandle);
402
+ if (forceKillHandle !== undefined) {
403
+ clearTimeout(forceKillHandle);
404
+ }
405
+ },
406
+ };
390
407
  }
391
408
 
392
409
  private timeoutResult(
@@ -53,6 +53,14 @@ export class HookSystem {
53
53
  private readonly runner: HookRunner;
54
54
  private readonly aggregator: HookAggregator;
55
55
  private eventHandler: HookEventHandler | null = null;
56
+ private initializationPromise: Promise<void> | undefined;
57
+ private initializationGeneration = 0;
58
+ private readonly initializationSignals = new Map<
59
+ number,
60
+ AbortSignal | undefined
61
+ >();
62
+ private lifecycleGeneration = 0;
63
+ private readonly disposalController = new AbortController();
56
64
 
57
65
  /**
58
66
  * @plan PLAN-20250218-HOOKSYSTEM.P03
@@ -97,22 +105,65 @@ export class HookSystem {
97
105
  * @requirement:HOOK-003 - Calls HookRegistry.initialize() to load hooks from config
98
106
  * @requirement:HOOK-008 - Called by trigger functions on first event fire
99
107
  */
100
- async initialize(): Promise<void> {
101
- debugLogger.debug('Initializing HookSystem');
108
+ initialize(signal?: AbortSignal): Promise<void> {
109
+ if (this.lifecycleGeneration > 0) {
110
+ return Promise.reject(new Error('HookSystem has been disposed.'));
111
+ }
112
+ if (signal?.aborted === true) {
113
+ return Promise.reject(signal.reason);
114
+ }
115
+ const generation = ++this.initializationGeneration;
116
+ this.initializationSignals.set(generation, signal);
117
+ if (this.initializationPromise !== undefined) {
118
+ return this.initializationPromise;
119
+ }
120
+ const initialization = this.runInitializationGenerations().finally(() => {
121
+ if (this.initializationPromise === initialization) {
122
+ this.initializationPromise = undefined;
123
+ }
124
+ });
125
+ this.initializationPromise = initialization;
126
+ return initialization;
127
+ }
102
128
 
103
- // Dispose old event handler to prevent MessageBus subscription leaks.
104
- // LLxprt enhancement: Upstream Gemini doesn't need this because their
105
- // HookEventHandler doesn't subscribe to MessageBus. LLxprt added MessageBus
106
- // integration in PLAN-20250218-HOOKSYSTEM.P03 (DELTA-HEVT-001).
107
- // Without disposal, each re-init creates a new subscription without
108
- // unsubscribing the old one, causing memory leaks.
109
- this.dispose();
129
+ private async runInitializationGenerations(): Promise<void> {
130
+ let completedGeneration = 0;
131
+ while (completedGeneration < this.initializationGeneration) {
132
+ const generation = this.initializationGeneration;
133
+ const requestedSignal = this.initializationSignals.get(generation);
134
+ const signal = requestedSignal
135
+ ? AbortSignal.any([requestedSignal, this.disposalController.signal])
136
+ : this.disposalController.signal;
137
+ try {
138
+ await this.initializeGeneration(signal);
139
+ completedGeneration = generation;
140
+ } finally {
141
+ this.deleteInitializationSignalsThrough(generation);
142
+ }
143
+ }
144
+ }
110
145
 
111
- // Initialize the registry (loads hooks from config)
112
- await this.registry.initialize();
146
+ private deleteInitializationSignalsThrough(generation: number): void {
147
+ for (const queuedGeneration of this.initializationSignals.keys()) {
148
+ if (queuedGeneration <= generation) {
149
+ this.initializationSignals.delete(queuedGeneration);
150
+ }
151
+ }
152
+ }
113
153
 
114
- // Create the event handler now that registry is ready,
115
- // forwarding injected dependencies per DELTA-HSYS-001
154
+ private async initializeGeneration(signal?: AbortSignal): Promise<void> {
155
+ const lifecycleGeneration = this.lifecycleGeneration;
156
+ if (lifecycleGeneration > 0) {
157
+ throw new Error('HookSystem has been disposed.');
158
+ }
159
+ signal?.throwIfAborted();
160
+ debugLogger.debug('Initializing HookSystem');
161
+ this.disposeEventHandler();
162
+ await this.registry.initialize(signal);
163
+ if (lifecycleGeneration !== this.lifecycleGeneration) {
164
+ throw new Error('HookSystem has been disposed.');
165
+ }
166
+ signal?.throwIfAborted();
116
167
  this.eventHandler = new HookEventHandler(
117
168
  this.config,
118
169
  this.registry,
@@ -387,6 +438,15 @@ export class HookSystem {
387
438
  * @requirement DELTA-HEVT-004
388
439
  */
389
440
  dispose(): void {
441
+ this.lifecycleGeneration++;
442
+ this.initializationGeneration++;
443
+ this.disposalController.abort(new Error('HookSystem has been disposed.'));
444
+ this.initializationSignals.clear();
445
+ this.disposeEventHandler();
446
+ }
447
+
448
+ private disposeEventHandler(): void {
390
449
  this.eventHandler?.dispose();
450
+ this.eventHandler = null;
391
451
  }
392
452
  }
package/src/index.ts CHANGED
@@ -146,6 +146,7 @@ export {
146
146
  } from './utils/generateContentResponseUtilities.js';
147
147
  export * from './utils/filesearch/fileSearch.js';
148
148
  export * from './utils/secure-browser-launcher.js';
149
+ export * from './utils/browser-profile-discovery.js';
149
150
  export * from './utils/errorParsing.js';
150
151
  export * from './utils/ignorePatterns.js';
151
152
  export { INITIAL_HISTORY_LENGTH } from './utils/environmentContext.js';
@@ -61,6 +61,9 @@ export interface GeminiPartExtension {
61
61
  * May also contain arbitrary user-configured field names (issue #2488).
62
62
  */
63
63
  llxprtSourceField?: string;
64
+ llxprtThoughtBlockId?: string;
65
+ llxprtThoughtBlockStatus?: 'delta' | 'complete';
66
+ llxprtThoughtIsHidden?: boolean;
64
67
  }
65
68
 
66
69
  /**
@@ -78,13 +78,15 @@ export async function createPolicyEngineConfig(
78
78
  export async function createPolicyEngineConfig(
79
79
  settings: PolicySettings,
80
80
  approvalMode: ApprovalMode,
81
- defaultPoliciesDir?: string,
81
+ defaultPoliciesDir: string | undefined,
82
+ trustedFolder: boolean,
82
83
  ): Promise<PolicyEngineConfig>;
83
84
 
84
85
  export async function createPolicyEngineConfig(
85
86
  configOrSettings: PolicyConfigSource | PolicySettings,
86
87
  approvalModeParam?: ApprovalMode,
87
88
  defaultPoliciesDir?: string,
89
+ trustedFolder?: boolean,
88
90
  ): Promise<PolicyEngineConfig> {
89
91
  const isPolicyConfigSource =
90
92
  typeof (configOrSettings as PolicyConfigSource).getApprovalMode ===
@@ -97,6 +99,7 @@ export async function createPolicyEngineConfig(
97
99
  configOrSettings as PolicySettings,
98
100
  approvalModeParam!,
99
101
  defaultPoliciesDir,
102
+ trustedFolder ?? false,
100
103
  );
101
104
  }
102
105
 
@@ -150,7 +153,8 @@ async function loadUserPolicyRules(
150
153
  async function buildSettingsRules(
151
154
  settings: PolicySettings,
152
155
  approvalMode: ApprovalMode,
153
- defaultPoliciesDir?: string,
156
+ defaultPoliciesDir: string | undefined,
157
+ trustedFolder: boolean,
154
158
  ): Promise<PolicyEngineConfig> {
155
159
  const policyDirs = getPolicyDirectories(defaultPoliciesDir);
156
160
 
@@ -167,7 +171,9 @@ async function buildSettingsRules(
167
171
  addMcpExcludedRules(settings, rules);
168
172
  addToolsExcludedRules(settings, rules);
169
173
  addToolsAllowedRules(settings, rules);
170
- addMcpTrustedRules(settings, rules);
174
+ if (trustedFolder) {
175
+ addMcpTrustedRules(settings, rules);
176
+ }
171
177
  addMcpAllowedRules(settings, rules);
172
178
 
173
179
  return {
@@ -194,7 +200,7 @@ function addMcpExcludedRules(
194
200
  if (settings.mcp?.excluded) {
195
201
  for (const serverName of settings.mcp.excluded) {
196
202
  rules.push({
197
- toolName: `${serverName}__*`,
203
+ toolNamePrefix: `${serverName}__`,
198
204
  decision: PolicyDecision.DENY,
199
205
  priority: 2.9,
200
206
  source: 'Settings (MCP Excluded)',
@@ -293,24 +299,31 @@ function normalizeToolName(toolName: string): string {
293
299
  return toolName;
294
300
  }
295
301
 
302
+ export const MCP_TRUSTED_POLICY_SOURCE = 'Settings (MCP Trusted)';
303
+
304
+ export function buildMcpTrustedRules(
305
+ settings: PolicySettings,
306
+ ): readonly PolicyRule[] {
307
+ return Object.entries(settings.mcpServers ?? {}).flatMap(
308
+ ([serverName, serverConfig]) =>
309
+ serverConfig.trust === true
310
+ ? [
311
+ {
312
+ toolNamePrefix: `${serverName}__`,
313
+ decision: PolicyDecision.ALLOW,
314
+ priority: 2.2,
315
+ source: MCP_TRUSTED_POLICY_SOURCE,
316
+ },
317
+ ]
318
+ : [],
319
+ );
320
+ }
321
+
296
322
  function addMcpTrustedRules(
297
323
  settings: PolicySettings,
298
324
  rules: PolicyRule[],
299
325
  ): void {
300
- if (settings.mcpServers) {
301
- for (const [serverName, serverConfig] of Object.entries(
302
- settings.mcpServers,
303
- )) {
304
- if (serverConfig.trust === true) {
305
- rules.push({
306
- toolName: `${serverName}__*`,
307
- decision: PolicyDecision.ALLOW,
308
- priority: 2.2,
309
- source: 'Settings (MCP Trusted)',
310
- });
311
- }
312
- }
313
- }
326
+ rules.push(...buildMcpTrustedRules(settings));
314
327
  }
315
328
 
316
329
  function addMcpAllowedRules(
@@ -320,7 +333,7 @@ function addMcpAllowedRules(
320
333
  if (settings.mcp?.allowed) {
321
334
  for (const serverName of settings.mcp.allowed) {
322
335
  rules.push({
323
- toolName: `${serverName}__*`,
336
+ toolNamePrefix: `${serverName}__`,
324
337
  decision: PolicyDecision.ALLOW,
325
338
  priority: 2.1,
326
339
  source: 'Settings (MCP Allowed)',