neoctl 0.2.37 → 0.2.38

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 (93) hide show
  1. package/dist/agents/agent-definition.d.ts +2 -0
  2. package/dist/agents/agent-definition.js +10 -0
  3. package/dist/agents/agent-definition.js.map +1 -1
  4. package/dist/agents/agent-tool.js +4 -0
  5. package/dist/agents/agent-tool.js.map +1 -1
  6. package/dist/agents/coordination-boundary.test.d.ts +1 -0
  7. package/dist/agents/coordination-boundary.test.js +58 -0
  8. package/dist/agents/coordination-boundary.test.js.map +1 -0
  9. package/dist/context/context-manager.d.ts +4 -0
  10. package/dist/context/context-manager.js +4 -1
  11. package/dist/context/context-manager.js.map +1 -1
  12. package/dist/context/prompt-config.d.ts +35 -0
  13. package/dist/context/prompt-config.js +151 -0
  14. package/dist/context/prompt-config.js.map +1 -0
  15. package/dist/context/prompt-config.test.d.ts +1 -0
  16. package/dist/context/prompt-config.test.js +124 -0
  17. package/dist/context/prompt-config.test.js.map +1 -0
  18. package/dist/context/prompts.d.ts +1 -1
  19. package/dist/context/prompts.js +4 -27
  20. package/dist/context/prompts.js.map +1 -1
  21. package/dist/context/smoke-context.js +3 -1
  22. package/dist/context/smoke-context.js.map +1 -1
  23. package/dist/context/system.md +16 -0
  24. package/dist/core/query-engine.d.ts +27 -1
  25. package/dist/core/query-engine.js +301 -109
  26. package/dist/core/query-engine.js.map +1 -1
  27. package/dist/core/query-session-settings.test.d.ts +1 -0
  28. package/dist/core/query-session-settings.test.js +252 -0
  29. package/dist/core/query-session-settings.test.js.map +1 -0
  30. package/dist/core/query.d.ts +16 -0
  31. package/dist/core/query.js +30 -0
  32. package/dist/core/query.js.map +1 -1
  33. package/dist/core/run-agent.js +3 -5
  34. package/dist/core/run-agent.js.map +1 -1
  35. package/dist/core/session-settings-prompt.d.ts +35 -0
  36. package/dist/core/session-settings-prompt.js +61 -0
  37. package/dist/core/session-settings-prompt.js.map +1 -0
  38. package/dist/core/session-settings-prompt.test.d.ts +1 -0
  39. package/dist/core/session-settings-prompt.test.js +257 -0
  40. package/dist/core/session-settings-prompt.test.js.map +1 -0
  41. package/dist/index.d.ts +3 -0
  42. package/dist/index.js +2 -0
  43. package/dist/index.js.map +1 -1
  44. package/dist/model/model-metadata.json +1 -3
  45. package/dist/model/openai-responses-instructions.test.d.ts +1 -0
  46. package/dist/model/openai-responses-instructions.test.js +64 -0
  47. package/dist/model/openai-responses-instructions.test.js.map +1 -0
  48. package/dist/model/openai-responses-mapper.js +5 -16
  49. package/dist/model/openai-responses-mapper.js.map +1 -1
  50. package/dist/model/smoke-responses-mapper.js +8 -5
  51. package/dist/model/smoke-responses-mapper.js.map +1 -1
  52. package/dist/session/session-export.d.ts +6 -0
  53. package/dist/session/session-export.js.map +1 -1
  54. package/dist/session/session-store.d.ts +11 -0
  55. package/dist/session/session-store.js +25 -0
  56. package/dist/session/session-store.js.map +1 -1
  57. package/dist/tools/builtins/exec-process-manager.js +4 -1
  58. package/dist/tools/builtins/exec-process-manager.js.map +1 -1
  59. package/dist/tools/terminal-background-history.test.d.ts +1 -0
  60. package/dist/tools/terminal-background-history.test.js +77 -0
  61. package/dist/tools/terminal-background-history.test.js.map +1 -0
  62. package/dist/tools/terminal-output-store.d.ts +4 -0
  63. package/dist/tools/terminal-output-store.js +18 -0
  64. package/dist/tools/terminal-output-store.js.map +1 -1
  65. package/dist/web/index.d.ts +22 -1
  66. package/dist/web/index.js +122 -12
  67. package/dist/web/index.js.map +1 -1
  68. package/dist/web/prompt-config-protocol.d.ts +4 -0
  69. package/dist/web/prompt-config-protocol.js +57 -0
  70. package/dist/web/prompt-config-protocol.js.map +1 -0
  71. package/dist/web/prompt-config-protocol.test.d.ts +1 -0
  72. package/dist/web/prompt-config-protocol.test.js +78 -0
  73. package/dist/web/prompt-config-protocol.test.js.map +1 -0
  74. package/dist/web/runtime-context-protocol.d.ts +1 -0
  75. package/dist/web/runtime-context-protocol.js +1 -0
  76. package/dist/web/runtime-context-protocol.js.map +1 -1
  77. package/dist/web/session-prompt-integration.test.d.ts +1 -0
  78. package/dist/web/session-prompt-integration.test.js +57 -0
  79. package/dist/web/session-prompt-integration.test.js.map +1 -0
  80. package/dist/web/session-prompt-protocol.d.ts +8 -0
  81. package/dist/web/session-prompt-protocol.js +11 -0
  82. package/dist/web/session-prompt-protocol.js.map +1 -0
  83. package/dist/web/session-settings.test.d.ts +1 -0
  84. package/dist/web/session-settings.test.js +254 -0
  85. package/dist/web/session-settings.test.js.map +1 -0
  86. package/dist/web/smoke-terminal-output-http.js +10 -5
  87. package/dist/web/smoke-terminal-output-http.js.map +1 -1
  88. package/dist/web/smoke-web-terminal-tasks.js +100 -3
  89. package/dist/web/smoke-web-terminal-tasks.js.map +1 -1
  90. package/docs/prompt-config.md +71 -0
  91. package/package.json +4 -3
  92. package/scripts/build-standalone.mjs +3 -0
  93. package/scripts/copy-prompt-resources.mjs +5 -0
@@ -1,3 +1,4 @@
1
+ import { randomUUID } from "node:crypto";
1
2
  import { InMemoryAppState } from "../app/app-state.js";
2
3
  import { InMemoryAppPromptStore } from "../app/app-prompt.js";
3
4
  import { AppPromptContextManager, DefaultContextManager, readProjectMemoryDocuments } from "../context/context-manager.js";
@@ -14,6 +15,7 @@ import { GENERAL_PURPOSE_AGENT } from "../agents/agent-definition.js";
14
15
  import { SessionStore } from "../session/session-store.js";
15
16
  import { buildPromptCacheDiagnostics } from "./prompt-cache-telemetry.js";
16
17
  import { computeStaticTokens } from "./context-metrics.js";
18
+ import { applySessionPrompt, createSessionPromptSnapshot, parseSessionPromptUpdate, SessionPromptError } from "./session-settings-prompt.js";
17
19
  const DEFAULT_SESSION_TITLE_DELAY_MS = 5000;
18
20
  export class QueryEngine {
19
21
  options;
@@ -25,14 +27,23 @@ export class QueryEngine {
25
27
  currentModel;
26
28
  currentReasoning;
27
29
  currentFastMode = false;
30
+ running = false;
31
+ pendingSessionSettings = {};
32
+ sessionPromptState;
33
+ activeSessionPrompt = null;
34
+ ephemeralSessionKey = randomUUID();
35
+ // Idle mutations and the next generator share a barrier. Never compact alongside a request.
36
+ settingsMutation = Promise.resolve();
28
37
  currentContextWindowTokens;
29
38
  currentModelGateway;
30
39
  sessionInitialized = false;
40
+ sessionInitialization;
31
41
  titleSchedulerVersion = 0;
32
42
  titleAgentRun;
33
43
  sessionTitleListeners = new Set();
34
44
  appPromptStore;
35
45
  contextManager;
46
+ baselineContextManager;
36
47
  additionalPromptContextManager;
37
48
  pendingCwdTransitionPaths;
38
49
  pendingRequestContext;
@@ -49,7 +60,12 @@ export class QueryEngine {
49
60
  ?? options.contextManager
50
61
  ?? new DefaultContextManager({ cwd: options.cwd });
51
62
  this.additionalPromptContextManager = new AdditionalPromptContextManager(baseContextManager, options.additionalPromptSections);
52
- this.contextManager = new AppPromptContextManager(this.additionalPromptContextManager, this.appPromptStore);
63
+ this.baselineContextManager = new AppPromptContextManager(this.additionalPromptContextManager, this.appPromptStore);
64
+ this.contextManager = { build: async (input) => {
65
+ // Capture before any asynchronous baseline work: an in-flight request never observes a later edit.
66
+ const content = this.activeSessionPrompt;
67
+ return applySessionPrompt(await this.baselineContextManager.build(input), content);
68
+ } };
53
69
  this.pendingCwdTransitionPaths = normalizeCwdTransitionPaths(options.cwdTransitionPaths);
54
70
  }
55
71
  forkForSession(sessionId, resume = true) {
@@ -77,25 +93,39 @@ export class QueryEngine {
77
93
  this.pendingRequestContextConsumed = onConsumed;
78
94
  }
79
95
  async initialize() {
96
+ if (this.sessionInitialization)
97
+ return this.sessionInitialization;
80
98
  if (this.sessionInitialized)
81
99
  return;
82
- this.sessionInitialized = true;
83
- if (this.options.session?.enabled === false)
84
- return;
85
- if (!this.options.session)
86
- return;
87
- await this.openSession({
88
- sessionId: this.options.session.sessionId,
89
- resume: this.options.session.resume,
90
- });
100
+ const initialization = (async () => {
101
+ if (this.options.session?.enabled !== false && this.options.session) {
102
+ await this.openSession({ sessionId: this.options.session.sessionId, resume: this.options.session.resume });
103
+ }
104
+ this.sessionInitialized = true;
105
+ })();
106
+ this.sessionInitialization = initialization;
107
+ try {
108
+ await initialization;
109
+ }
110
+ finally {
111
+ this.sessionInitialization = undefined;
112
+ }
91
113
  }
92
114
  async newSession() {
93
- this.sessionInitialized = true;
94
- await this.openSession({ resume: false });
95
- const snapshot = this.sessionStore?.snapshot();
96
- if (!snapshot)
97
- throw new Error("session transcripts are disabled");
98
- return snapshot;
115
+ if (this.running)
116
+ throw new Error("cannot switch session while query engine is running");
117
+ const mutation = this.settingsMutation.then(async () => {
118
+ await this.sessionInitialization;
119
+ await this.openSession({ resume: false });
120
+ this.sessionInitialized = true;
121
+ const snapshot = this.sessionStore?.snapshot();
122
+ if (!snapshot)
123
+ throw new Error("session transcripts are disabled");
124
+ return snapshot;
125
+ });
126
+ // Session switches share the editor barrier: stale saves can never land in a new session.
127
+ this.settingsMutation = mutation.catch(() => undefined);
128
+ return mutation;
99
129
  }
100
130
  async listSessions(limit = 10) {
101
131
  if (this.options.session?.enabled === false || !this.options.session)
@@ -121,92 +151,241 @@ export class QueryEngine {
121
151
  });
122
152
  }
123
153
  async *sendUserText(text, options = {}) {
124
- await this.initialize();
125
- const cwdTransitionPaths = this.pendingCwdTransitionPaths;
126
- const pendingRequestContext = this.pendingRequestContext;
127
- const pendingRequestContextConsumed = this.pendingRequestContextConsumed;
128
- this.pendingCwdTransitionPaths = undefined;
129
- this.options.cwdTransitionPaths = undefined;
130
- if (cwdTransitionPaths?.length)
131
- void this.options.onCwdTransitionConsumed?.();
132
- const userMessage = options.blocks
133
- ? await this.persistMessageImages({
134
- ...createTextMessage("user", options.displayText ?? text),
135
- blocks: options.blocks,
136
- })
137
- : createTextMessage("user", text);
138
- this.history.push(userMessage);
139
- this.sessionStore?.recordMessage(userMessage);
140
- this.scheduleSessionTitleCheck();
141
- const initMessage = createSystemInitMessage({
142
- agentId: this.agentId,
143
- tools: this.options.tools.names(),
144
- model: this.currentModel,
145
- commands: [...(this.options.commands ?? [])],
146
- agents: [...(this.options.agents ?? [])],
147
- skills: [...(this.options.skills ?? [])],
148
- plugins: [...(this.options.plugins ?? [])],
149
- });
150
- initMessage.metadata = {
151
- ...initMessage.metadata,
152
- reasoning: cloneReasoningConfig(this.currentReasoning),
153
- };
154
- yield {
155
- type: "message",
156
- message: initMessage,
157
- };
158
- this.sessionStore?.recordMessage(initMessage);
159
- const queryOptions = {
160
- agentId: this.agentId,
161
- model: this.currentModel,
162
- reasoning: cloneReasoningConfig(this.currentReasoning),
163
- queryOrigin: this.options.queryOrigin ?? "repl",
164
- serviceTier: this.currentFastMode ? "priority" : undefined,
165
- maxOutputTokensOverride: this.options.maxOutputTokensOverride,
166
- contextWindowTokensOverride: this.currentContextWindowTokens,
167
- maxTurns: this.options.maxTurns,
168
- workspaceCwd: this.options.cwd,
169
- requestContext: pendingRequestContext || cwdTransitionPaths?.length
170
- ? {
171
- ...pendingRequestContext,
172
- ...(cwdTransitionPaths?.length
173
- ? { cwdTransition: { paths: cwdTransitionPaths, current: cwdTransitionPaths.at(-1) } }
174
- : {}),
154
+ if (this.running)
155
+ throw new Error("query engine is already running");
156
+ this.running = true;
157
+ try {
158
+ await this.settingsMutation;
159
+ await this.initialize();
160
+ const cwdTransitionPaths = this.pendingCwdTransitionPaths;
161
+ const pendingRequestContext = this.pendingRequestContext;
162
+ const pendingRequestContextConsumed = this.pendingRequestContextConsumed;
163
+ this.pendingCwdTransitionPaths = undefined;
164
+ this.options.cwdTransitionPaths = undefined;
165
+ if (cwdTransitionPaths?.length)
166
+ void this.options.onCwdTransitionConsumed?.();
167
+ const userMessage = options.blocks
168
+ ? await this.persistMessageImages({
169
+ ...createTextMessage("user", options.displayText ?? text),
170
+ blocks: options.blocks,
171
+ })
172
+ : createTextMessage("user", text);
173
+ this.history.push(userMessage);
174
+ this.sessionStore?.recordMessage(userMessage);
175
+ this.scheduleSessionTitleCheck();
176
+ const initMessage = createSystemInitMessage({
177
+ agentId: this.agentId,
178
+ tools: this.options.tools.names(),
179
+ model: this.currentModel,
180
+ commands: [...(this.options.commands ?? [])],
181
+ agents: [...(this.options.agents ?? [])],
182
+ skills: [...(this.options.skills ?? [])],
183
+ plugins: [...(this.options.plugins ?? [])],
184
+ });
185
+ initMessage.metadata = {
186
+ ...initMessage.metadata,
187
+ reasoning: cloneReasoningConfig(this.currentReasoning),
188
+ };
189
+ yield {
190
+ type: "message",
191
+ message: initMessage,
192
+ };
193
+ this.sessionStore?.recordMessage(initMessage);
194
+ const queryOptions = {
195
+ agentId: this.agentId,
196
+ model: this.currentModel,
197
+ reasoning: cloneReasoningConfig(this.currentReasoning),
198
+ queryOrigin: this.options.queryOrigin ?? "repl",
199
+ serviceTier: this.currentFastMode ? "priority" : undefined,
200
+ maxOutputTokensOverride: this.options.maxOutputTokensOverride,
201
+ contextWindowTokensOverride: this.currentContextWindowTokens,
202
+ maxTurns: this.options.maxTurns,
203
+ workspaceCwd: this.options.cwd,
204
+ requestContext: pendingRequestContext || cwdTransitionPaths?.length
205
+ ? {
206
+ ...pendingRequestContext,
207
+ ...(cwdTransitionPaths?.length
208
+ ? { cwdTransition: { paths: cwdTransitionPaths, current: cwdTransitionPaths.at(-1) } }
209
+ : {}),
210
+ }
211
+ : undefined,
212
+ onRequestContextConsumed: pendingRequestContext ? () => {
213
+ pendingRequestContextConsumed?.();
214
+ if (this.pendingRequestContext === pendingRequestContext) {
215
+ this.pendingRequestContext = undefined;
216
+ this.pendingRequestContextConsumed = undefined;
217
+ }
218
+ } : undefined,
219
+ abortSignal: options.abortSignal,
220
+ stopAfterTurn: options.stopAfterTurn,
221
+ beforeTurn: (messages) => this.consumeSessionSettings(messages),
222
+ };
223
+ const stream = query(this.history, {
224
+ ...this.options,
225
+ contextManager: this.contextManager,
226
+ modelGateway: this.currentModelGateway,
227
+ taskNotificationSource: this.options.taskNotificationSource,
228
+ toolResultMemory: this.sessionStore?.toolResultMemory,
229
+ session: this.sessionStore ? { sessionId: this.sessionStore.sessionId, sessionDir: this.sessionStore.sessionDir, rootDir: this.options.session?.rootDir } : undefined,
230
+ recordContentReplacements: (records) => this.sessionStore?.recordContentReplacements(records),
231
+ exportToolCalls: (calls) => this.recordSyntheticToolCalls(calls),
232
+ applyCompaction: (result) => this.applyCompactionResult(result),
233
+ }, queryOptions);
234
+ for await (const event of stream) {
235
+ if (event.type === "message") {
236
+ const message = await this.persistMessageImages(event.message);
237
+ this.history.push(message);
238
+ this.sessionStore?.recordMessage(message);
239
+ yield { ...event, message };
240
+ continue;
175
241
  }
176
- : undefined,
177
- onRequestContextConsumed: pendingRequestContext ? () => {
178
- pendingRequestContextConsumed?.();
179
- if (this.pendingRequestContext === pendingRequestContext) {
180
- this.pendingRequestContext = undefined;
181
- this.pendingRequestContextConsumed = undefined;
242
+ if (event.type === "terminal") {
243
+ this.lastTerminalReason = event.reason;
244
+ // The final assistant/tool messages have already been recorded by this wrapper.
245
+ const hadPending = Object.keys(this.pendingSessionSettings).length > 0;
246
+ for (const result of await this.flushSessionSettings()) {
247
+ if (result.report)
248
+ yield { type: "context.compacted", compaction: result.report };
249
+ }
250
+ if (hadPending)
251
+ yield { type: "context.metrics", metrics: await this.contextMetrics() };
182
252
  }
183
- } : undefined,
184
- abortSignal: options.abortSignal,
185
- stopAfterTurn: options.stopAfterTurn,
186
- };
187
- const stream = query(this.history, {
188
- ...this.options,
189
- contextManager: this.contextManager,
190
- modelGateway: this.currentModelGateway,
191
- taskNotificationSource: this.options.taskNotificationSource,
192
- toolResultMemory: this.sessionStore?.toolResultMemory,
193
- session: this.sessionStore ? { sessionId: this.sessionStore.sessionId, sessionDir: this.sessionStore.sessionDir, rootDir: this.options.session?.rootDir } : undefined,
194
- recordContentReplacements: (records) => this.sessionStore?.recordContentReplacements(records),
195
- exportToolCalls: (calls) => this.recordSyntheticToolCalls(calls),
196
- applyCompaction: (result) => this.applyCompactionResult(result),
197
- }, queryOptions);
198
- for await (const event of stream) {
199
- if (event.type === "message") {
200
- const message = await this.persistMessageImages(event.message);
201
- this.history.push(message);
202
- this.sessionStore?.recordMessage(message);
203
- yield { ...event, message };
204
- continue;
253
+ yield event;
254
+ }
255
+ }
256
+ finally {
257
+ // Covers initialization errors, aborts, exceptions and consumer generator.return().
258
+ // Do not yield in finally: a closing consumer may never resume the generator.
259
+ try {
260
+ do {
261
+ await this.flushSessionSettings();
262
+ } while (Object.keys(this.pendingSessionSettings).length);
263
+ }
264
+ finally {
265
+ this.running = false;
266
+ }
267
+ }
268
+ }
269
+ /** Queue instance-local settings; no prompt messages or provider-global mutation. */
270
+ async updateSessionSettings(patch) {
271
+ const normalized = {};
272
+ if (Object.hasOwn(patch, "model"))
273
+ normalized.model = patch.model?.trim() || undefined;
274
+ if (Object.hasOwn(patch, "reasoning"))
275
+ normalized.reasoning = cloneReasoningConfig(patch.reasoning);
276
+ if (Object.hasOwn(patch, "fastMode"))
277
+ normalized.fastMode = patch.fastMode === true;
278
+ if (Object.hasOwn(patch, "contextWindowTokens")) {
279
+ const tokens = normalizePositiveInteger(patch.contextWindowTokens);
280
+ if (!tokens)
281
+ throw new Error("context window tokens must be a positive integer");
282
+ normalized.contextWindowTokens = tokens;
283
+ }
284
+ if (patch.compact)
285
+ normalized.compact = true;
286
+ if (this.running) {
287
+ this.pendingSessionSettings = { ...this.pendingSessionSettings, ...normalized };
288
+ return { deferred: true };
289
+ }
290
+ const mutation = this.settingsMutation.then(async () => {
291
+ await this.initialize();
292
+ const update = await this.applySessionSettings(normalized, this.getHistoryMessages());
293
+ if (update?.compaction)
294
+ this.applyCompactionResult(update.compaction);
295
+ return { deferred: false, ...(update?.compaction ? { compaction: update.compaction } : {}) };
296
+ });
297
+ // A rejected mutation must not poison subsequent settings or user input.
298
+ this.settingsMutation = mutation.catch(() => undefined);
299
+ return mutation;
300
+ }
301
+ /** Editor state includes accepted queued changes; runtime exports still describe the active turn. */
302
+ async getSessionPrompt() {
303
+ await this.settingsMutation;
304
+ await this.initialize();
305
+ const snapshot = await this.buildPromptExportSnapshot(this.getHistoryMessages());
306
+ return createSessionPromptSnapshot(this.sessionPromptKey(), this.sessionPromptState, snapshot.baseSystemPrompt ?? "");
307
+ }
308
+ async updateSessionPrompt(input) {
309
+ const update = parseSessionPromptUpdate(input);
310
+ const mutation = this.settingsMutation.then(async () => {
311
+ await this.initialize();
312
+ const key = this.sessionPromptKey();
313
+ const snapshot = await this.buildPromptExportSnapshot(this.getHistoryMessages());
314
+ const current = createSessionPromptSnapshot(key, this.sessionPromptState, snapshot.baseSystemPrompt ?? "");
315
+ if (key !== this.sessionPromptKey() || update.revision !== current.revision) {
316
+ throw new SessionPromptError("session prompt changed; reload before saving", 409);
317
+ }
318
+ const state = { content: update.reset ? null : update.content, revision: randomUUID() };
319
+ // Persist before accepting, including busy edits, so same-session resume cannot lose them.
320
+ this.sessionStore?.recordSessionPrompt(state);
321
+ this.sessionPromptState = state;
322
+ const deferred = this.running;
323
+ if (deferred)
324
+ this.pendingSessionSettings = { ...this.pendingSessionSettings, sessionPrompt: state.content };
325
+ else
326
+ this.activeSessionPrompt = state.content;
327
+ return { ok: true, ...createSessionPromptSnapshot(key, state, snapshot.baseSystemPrompt ?? ""), deferred };
328
+ });
329
+ this.settingsMutation = mutation.catch(() => undefined);
330
+ return mutation;
331
+ }
332
+ sessionPromptKey() {
333
+ return this.sessionStore ? `${this.sessionStore.sessionDir}:${this.agentId}` : this.ephemeralSessionKey;
334
+ }
335
+ getPendingSessionSettings() {
336
+ const patch = { ...this.pendingSessionSettings };
337
+ if (Object.hasOwn(patch, "reasoning"))
338
+ patch.reasoning = cloneReasoningConfig(patch.reasoning);
339
+ return patch;
340
+ }
341
+ async consumeSessionSettings(messages) {
342
+ const patch = this.pendingSessionSettings;
343
+ if (!Object.keys(patch).length)
344
+ return undefined;
345
+ this.pendingSessionSettings = {};
346
+ return this.applySessionSettings(patch, messages);
347
+ }
348
+ async applySessionSettings(patch, messages) {
349
+ const changed = {};
350
+ if (Object.hasOwn(patch, "sessionPrompt"))
351
+ this.activeSessionPrompt = patch.sessionPrompt ?? null;
352
+ if (Object.hasOwn(patch, "model") && patch.model !== this.currentModel) {
353
+ this.currentModel = patch.model;
354
+ changed.model = patch.model;
355
+ }
356
+ if (Object.hasOwn(patch, "reasoning") && !sameReasoningConfig(patch.reasoning, this.currentReasoning)) {
357
+ this.currentReasoning = cloneReasoningConfig(patch.reasoning);
358
+ changed.reasoning = cloneReasoningConfig(patch.reasoning);
359
+ }
360
+ if (Object.hasOwn(patch, "fastMode") && patch.fastMode !== this.currentFastMode) {
361
+ this.currentFastMode = patch.fastMode === true;
362
+ this.sessionStore?.recordFastMode(this.currentFastMode);
363
+ changed.fastMode = this.currentFastMode;
364
+ }
365
+ if (Object.hasOwn(patch, "contextWindowTokens") && patch.contextWindowTokens !== this.currentContextWindowTokens) {
366
+ this.currentContextWindowTokens = patch.contextWindowTokens;
367
+ this.sessionStore?.recordContextWindowTokens(this.currentContextWindowTokens);
368
+ changed.contextWindowTokens = this.currentContextWindowTokens;
369
+ }
370
+ let compaction;
371
+ if (patch.compact) {
372
+ const compactor = this.options.compactor ?? new ModelDrivenCompactor(this.currentModelGateway);
373
+ const budget = { ...this.options.contextBudget, contextWindowTokens: this.currentContextWindowTokens ?? this.options.contextBudget?.contextWindowTokens };
374
+ compaction = withCompactionReport(await (compactor.manualCompact?.(messages, budget) ?? compactor.compact(messages, budget)), messages.length);
375
+ }
376
+ return Object.keys(changed).length || compaction ? { settings: changed, compaction } : undefined;
377
+ }
378
+ async flushSessionSettings() {
379
+ const results = [];
380
+ while (Object.keys(this.pendingSessionSettings).length) {
381
+ const update = await this.consumeSessionSettings(this.getHistoryMessages());
382
+ if (update?.compaction) {
383
+ this.applyCompactionResult(update.compaction);
384
+ if (update.compaction.changed)
385
+ results.push(update.compaction);
205
386
  }
206
- if (event.type === "terminal")
207
- this.lastTerminalReason = event.reason;
208
- yield event;
209
387
  }
388
+ return results;
210
389
  }
211
390
  setModel(model, reasoning, updateReasoning = false) {
212
391
  this.currentModel = model?.trim() || undefined;
@@ -264,6 +443,9 @@ export class QueryEngine {
264
443
  return this.appPromptStore.snapshot();
265
444
  }
266
445
  reset() {
446
+ if (this.running)
447
+ throw new Error("cannot reset while query engine is running");
448
+ this.pendingSessionSettings = {};
267
449
  this.history.length = 0;
268
450
  this.lastTerminalReason = undefined;
269
451
  this.cancelPendingTitleWork();
@@ -271,17 +453,10 @@ export class QueryEngine {
271
453
  this.notifySessionTitleChange(this.sessionStore?.snapshot());
272
454
  }
273
455
  async compact(options = {}) {
274
- await this.initialize();
275
456
  if (options.abortSignal?.aborted)
276
457
  return { messages: this.getHistoryMessages(), changed: false, reason: "none" };
277
- const compactor = this.options.compactor ?? new ModelDrivenCompactor(this.currentModelGateway);
278
- const contextBudget = {
279
- ...this.options.contextBudget,
280
- contextWindowTokens: this.currentContextWindowTokens ?? this.options.contextBudget?.contextWindowTokens,
281
- };
282
- const result = withCompactionReport(await (compactor.manualCompact?.(this.history, contextBudget) ?? compactor.compact(this.history, contextBudget)), this.history.length);
283
- this.applyCompactionResult(result);
284
- return result;
458
+ const result = await this.updateSessionSettings({ compact: true });
459
+ return result.compaction ?? { messages: this.getHistoryMessages(), changed: false, reason: "none" };
285
460
  }
286
461
  async pureCompact(options = {}) {
287
462
  await this.initialize();
@@ -376,12 +551,15 @@ export class QueryEngine {
376
551
  emit: () => undefined,
377
552
  };
378
553
  const initialToolDefinitions = this.options.tools.definitions(toolContext);
379
- const context = await this.contextManager.build({
554
+ const activePrompt = this.activeSessionPrompt;
555
+ const baseline = await this.baselineContextManager.build({
380
556
  agentId: this.agentId,
381
557
  messages,
382
558
  enabledTools: initialToolDefinitions.map((tool) => tool.name),
383
559
  toolUseContext: toolContext,
384
560
  });
561
+ const context = applySessionPrompt(baseline, activePrompt);
562
+ const desiredPrompt = createSessionPromptSnapshot(this.sessionPromptKey(), this.sessionPromptState, baseline.systemPrompt);
385
563
  const toolDefinitions = this.options.tools.definitions(toolContext);
386
564
  const messagesWithUserContext = applyRuntimeContextForPromptCache([], context.userContext, {});
387
565
  const userContextPrompt = messagesWithUserContext[0]?.blocks
@@ -392,7 +570,13 @@ export class QueryEngine {
392
570
  model: this.currentModel,
393
571
  reasoning: cloneReasoningConfig(this.currentReasoning),
394
572
  systemPrompt: context.systemPrompt,
395
- baseSystemPrompt: context.systemPrompt,
573
+ baseSystemPrompt: baseline.systemPrompt,
574
+ sessionPrompt: {
575
+ override: activePrompt !== null,
576
+ revision: desiredPrompt.revision,
577
+ deferred: Object.hasOwn(this.pendingSessionSettings, "sessionPrompt"),
578
+ pendingOverride: desiredPrompt.override,
579
+ },
396
580
  promptSections: context.promptSections,
397
581
  userContext: context.userContext,
398
582
  systemContext: context.systemContext,
@@ -449,6 +633,9 @@ export class QueryEngine {
449
633
  resume: options.resume,
450
634
  toolResultThresholdChars: this.options.session.toolResultThresholdChars,
451
635
  });
636
+ this.pendingSessionSettings = {};
637
+ this.sessionPromptState = this.sessionStore.getSessionPrompt();
638
+ this.activeSessionPrompt = this.sessionPromptState?.content ?? null;
452
639
  this.history.length = 0;
453
640
  if (options.resume)
454
641
  this.history.push(...this.sessionStore.getInitialMessages());
@@ -609,6 +796,11 @@ function resolveSessionTitleDelayMs() {
609
796
  }
610
797
  return DEFAULT_SESSION_TITLE_DELAY_MS;
611
798
  }
799
+ function sameReasoningConfig(a, b) {
800
+ if (a === null || b === null || a === undefined || b === undefined)
801
+ return a === b;
802
+ return a.effort === b.effort && a.summary === b.summary;
803
+ }
612
804
  function cloneReasoningConfig(reasoning) {
613
805
  if (reasoning === null)
614
806
  return null;