neoctl 0.2.45 → 0.2.47

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 (102) hide show
  1. package/dist/agents/agent-tool.js +2 -1
  2. package/dist/agents/agent-tool.js.map +1 -1
  3. package/dist/context/compaction.d.ts +6 -0
  4. package/dist/context/compaction.js +10 -5
  5. package/dist/context/compaction.js.map +1 -1
  6. package/dist/context/prompts.js +2 -0
  7. package/dist/context/prompts.js.map +1 -1
  8. package/dist/core/abortable-stream.d.ts +4 -0
  9. package/dist/core/abortable-stream.js +52 -0
  10. package/dist/core/abortable-stream.js.map +1 -0
  11. package/dist/core/compaction-progress.d.ts +5 -0
  12. package/dist/core/compaction-progress.js +38 -0
  13. package/dist/core/compaction-progress.js.map +1 -0
  14. package/dist/core/execution-state.d.ts +12 -0
  15. package/dist/core/execution-state.js +19 -0
  16. package/dist/core/execution-state.js.map +1 -0
  17. package/dist/core/image-registry.d.ts +4 -2
  18. package/dist/core/image-registry.js +32 -7
  19. package/dist/core/image-registry.js.map +1 -1
  20. package/dist/core/image-storage.js +4 -0
  21. package/dist/core/image-storage.js.map +1 -1
  22. package/dist/core/query-engine.d.ts +31 -1
  23. package/dist/core/query-engine.js +198 -70
  24. package/dist/core/query-engine.js.map +1 -1
  25. package/dist/core/query.d.ts +5 -1
  26. package/dist/core/query.js +77 -22
  27. package/dist/core/query.js.map +1 -1
  28. package/dist/core/session-images.d.ts +52 -0
  29. package/dist/core/session-images.js +395 -0
  30. package/dist/core/session-images.js.map +1 -0
  31. package/dist/index.d.ts +1 -0
  32. package/dist/index.js +1 -0
  33. package/dist/index.js.map +1 -1
  34. package/dist/model/model-gateway.d.ts +4 -0
  35. package/dist/model/model-gateway.js.map +1 -1
  36. package/dist/model/openai-chat-mapper.js +2 -0
  37. package/dist/model/openai-chat-mapper.js.map +1 -1
  38. package/dist/model/openai-mappers.js +0 -2
  39. package/dist/model/openai-mappers.js.map +1 -1
  40. package/dist/model/openai-responses-mapper.js +3 -0
  41. package/dist/model/openai-responses-mapper.js.map +1 -1
  42. package/dist/model/tool-input-stream.d.ts +31 -0
  43. package/dist/model/tool-input-stream.js +226 -0
  44. package/dist/model/tool-input-stream.js.map +1 -0
  45. package/dist/repl/index.js +2 -0
  46. package/dist/repl/index.js.map +1 -1
  47. package/dist/session/session-store.d.ts +9 -0
  48. package/dist/session/session-store.js +20 -2
  49. package/dist/session/session-store.js.map +1 -1
  50. package/dist/tasks/subagent-tools.js +1 -1
  51. package/dist/tasks/subagent-tools.js.map +1 -1
  52. package/dist/tools/builtins/exec-process-manager.d.ts +4 -1
  53. package/dist/tools/builtins/exec-process-manager.js +26 -2
  54. package/dist/tools/builtins/exec-process-manager.js.map +1 -1
  55. package/dist/tools/builtins/exec-tool.js +1 -1
  56. package/dist/tools/builtins/exec-tool.js.map +1 -1
  57. package/dist/tools/builtins/framework-feedback-tool.d.ts +26 -0
  58. package/dist/tools/builtins/framework-feedback-tool.js +97 -0
  59. package/dist/tools/builtins/framework-feedback-tool.js.map +1 -0
  60. package/dist/tools/builtins/image-generation-tool.d.ts +2 -0
  61. package/dist/tools/builtins/image-generation-tool.js +31 -141
  62. package/dist/tools/builtins/image-generation-tool.js.map +1 -1
  63. package/dist/tools/builtins/image-loader-tool.d.ts +6 -1
  64. package/dist/tools/builtins/image-loader-tool.js +48 -26
  65. package/dist/tools/builtins/image-loader-tool.js.map +1 -1
  66. package/dist/tools/builtins/image-note-tool.js +2 -2
  67. package/dist/tools/builtins/image-note-tool.js.map +1 -1
  68. package/dist/tools/run-tool-use.js +4 -2
  69. package/dist/tools/run-tool-use.js.map +1 -1
  70. package/dist/tools/terminal-output-store.d.ts +1 -0
  71. package/dist/tools/terminal-output-store.js +13 -1
  72. package/dist/tools/terminal-output-store.js.map +1 -1
  73. package/dist/tools/tool-catalog.d.ts +1 -0
  74. package/dist/tools/tool-catalog.js +2 -0
  75. package/dist/tools/tool-catalog.js.map +1 -1
  76. package/dist/tools/tool.d.ts +1 -0
  77. package/dist/tools/tool.js.map +1 -1
  78. package/dist/types/events.d.ts +11 -0
  79. package/dist/types/messages.d.ts +3 -1
  80. package/dist/types/messages.js.map +1 -1
  81. package/dist/web/demo-mode.d.ts +37 -0
  82. package/dist/web/demo-mode.js +141 -0
  83. package/dist/web/demo-mode.js.map +1 -0
  84. package/dist/web/demo-tools.d.ts +37 -0
  85. package/dist/web/demo-tools.js +259 -0
  86. package/dist/web/demo-tools.js.map +1 -0
  87. package/dist/web/demo-video.d.ts +1 -0
  88. package/dist/web/demo-video.js +3 -0
  89. package/dist/web/demo-video.js.map +1 -0
  90. package/dist/web/image-result-metadata.d.ts +6 -0
  91. package/dist/web/image-result-metadata.js +3 -0
  92. package/dist/web/image-result-metadata.js.map +1 -1
  93. package/dist/web/index.d.ts +57 -3
  94. package/dist/web/index.js +388 -24
  95. package/dist/web/index.js.map +1 -1
  96. package/dist/web/tool-activity.d.ts +9 -0
  97. package/dist/web/tool-activity.js +19 -0
  98. package/dist/web/tool-activity.js.map +1 -0
  99. package/dist/web/tool-observation-result.d.ts +1 -0
  100. package/dist/web/tool-observation-result.js +35 -0
  101. package/dist/web/tool-observation-result.js.map +1 -0
  102. package/package.json +1 -1
@@ -1,4 +1,6 @@
1
1
  import { randomUUID } from "node:crypto";
2
+ import { performance } from "node:perf_hooks";
3
+ import { executionRecoveryHint, isResumableExecution } from "./execution-state.js";
2
4
  import { InMemoryAppState } from "../app/app-state.js";
3
5
  import { InMemoryAppPromptStore } from "../app/app-prompt.js";
4
6
  import { AppPromptContextManager, DefaultContextManager, readProjectMemoryDocuments } from "../context/context-manager.js";
@@ -8,7 +10,8 @@ import { ToolRegistry } from "../tools/registry.js";
8
10
  import { createSystemInitMessage, createTextMessage } from "../types/messages.js";
9
11
  import { buildContextMetrics } from "./context-metrics.js";
10
12
  import { applyRuntimeContextForPromptCache } from "./message-pipeline.js";
11
- import { persistMessageImages } from "./image-storage.js";
13
+ import { abortableWork } from "./abortable-stream.js";
14
+ import { SessionImageStore } from "./session-images.js";
12
15
  import { query } from "./query.js";
13
16
  import { runAgent } from "./run-agent.js";
14
17
  import { GENERAL_PURPOSE_AGENT } from "../agents/agent-definition.js";
@@ -19,13 +22,58 @@ import { applySessionPrompt, sessionPromptMode, createSessionPromptSnapshot, par
19
22
  const DEFAULT_SESSION_TITLE_DELAY_MS = 5000;
20
23
  export class QueryEngine {
21
24
  options;
25
+ simulation;
26
+ displayOnlyMessages = [];
27
+ /** Instance-local execution boundary; never inherited by forkForSession or written to provider settings. */
28
+ setSimulation(simulation) {
29
+ if (this.running)
30
+ throw new Error("cannot replace execution while a query is running");
31
+ this.cancelPendingTitleWork();
32
+ this.simulation = simulation;
33
+ }
34
+ /** Persist a visible message without submitting it to a model or changing an in-flight context. */
35
+ async recordDisplayMessage(message) {
36
+ await this.initialize();
37
+ const store = this.sessionStore;
38
+ const windowId = store?.snapshot().firstWindowId;
39
+ const stored = await this.imageStore.persistMessage({ ...message, metadata: { ...message.metadata, displayOnly: true } });
40
+ if (store !== this.sessionStore || windowId !== store?.snapshot().firstWindowId)
41
+ throw new Error("Session changed while recording message");
42
+ if (store)
43
+ store.recordMessage(stored);
44
+ else
45
+ this.displayOnlyMessages.push(stored);
46
+ return stored;
47
+ }
22
48
  agentId;
23
49
  history = [];
24
50
  titleTimers = new Set();
25
51
  lastTerminalReason;
52
+ executionState;
53
+ executionStartedAt;
54
+ executionNow() {
55
+ return this.options.timingClock?.monotonicNow() ?? performance.now();
56
+ }
57
+ getExecutionState() {
58
+ if (!this.executionState)
59
+ return undefined;
60
+ const elapsedMs = (this.executionState.elapsedMs ?? 0) + (this.executionStartedAt === undefined
61
+ ? 0 : Math.max(0, this.executionNow() - this.executionStartedAt));
62
+ return { ...this.executionState, elapsedMs: Math.floor(elapsedMs) };
63
+ }
64
+ markExecutionPaused() {
65
+ this.setExecutionState({ status: "paused", reason: "user_pause" });
66
+ }
67
+ setExecutionState(state) {
68
+ const elapsedMs = state.elapsedMs ?? this.getExecutionState()?.elapsedMs ?? 0;
69
+ this.executionState = { ...state, elapsedMs, ...(this.executionState?.resumeCompaction ? { resumeCompaction: true } : {}) };
70
+ this.executionStartedAt = state.status === "running" ? this.executionNow() : undefined;
71
+ this.sessionStore?.recordExecutionState(this.executionState);
72
+ }
26
73
  queryTiming;
27
74
  timingRecords = new Map();
28
75
  sessionStore;
76
+ imageStore;
29
77
  currentModel;
30
78
  currentReasoning;
31
79
  currentFastMode = false;
@@ -103,6 +151,7 @@ export class QueryEngine {
103
151
  if (this.options.session?.enabled !== false && this.options.session) {
104
152
  await this.openSession({ sessionId: this.options.session.sessionId, resume: this.options.session.resume });
105
153
  }
154
+ this.imageStore ??= await SessionImageStore.open(undefined, this.agentId);
106
155
  this.sessionInitialized = true;
107
156
  })();
108
157
  this.sessionInitialization = initialization;
@@ -152,13 +201,35 @@ export class QueryEngine {
152
201
  rootDir: this.options.session.rootDir,
153
202
  });
154
203
  }
155
- async *sendUserText(text, options = {}) {
204
+ sendUserText(text, options = {}) {
205
+ return this.runQuery(text, options);
206
+ }
207
+ /** A UI continuation is a runtime action, not an invented user utterance. */
208
+ continueExecution(options = {}) {
209
+ return this.runQuery(undefined, options);
210
+ }
211
+ async *runQuery(text, options = {}) {
156
212
  if (this.running)
157
213
  throw new Error("query engine is already running");
158
214
  this.running = true;
215
+ let terminalReceived = false;
216
+ let queryAccepted = text !== undefined;
159
217
  try {
160
218
  await this.settingsMutation;
161
219
  await this.initialize();
220
+ const recovery = isResumableExecution(this.executionState) ? this.executionState : undefined;
221
+ if (text === undefined && !recovery)
222
+ throw new Error("No interrupted execution to continue");
223
+ queryAccepted = true;
224
+ if (recovery) {
225
+ this.pendingRequestContext = { ...this.pendingRequestContext,
226
+ executionRecovery: executionRecoveryHint(recovery),
227
+ ...(text === undefined ? { continuationAction: "用户点击了继续按钮,请继续尚未完成的任务。这是运行时动作,不是用户发送的新消息。" } : {}),
228
+ };
229
+ }
230
+ const elapsedMs = recovery?.elapsedMs ?? 0;
231
+ this.setExecutionState(options.abortSignal?.aborted
232
+ ? { status: "paused", reason: "user_pause", elapsedMs } : { status: "running", elapsedMs });
162
233
  const cwdTransitionPaths = this.pendingCwdTransitionPaths;
163
234
  const pendingRequestContext = this.pendingRequestContext;
164
235
  const pendingRequestContextConsumed = this.pendingRequestContextConsumed;
@@ -166,18 +237,20 @@ export class QueryEngine {
166
237
  this.options.cwdTransitionPaths = undefined;
167
238
  if (cwdTransitionPaths?.length)
168
239
  void this.options.onCwdTransitionConsumed?.();
169
- const userMessage = options.blocks
170
- ? await this.persistMessageImages({
171
- ...createTextMessage("user", options.displayText ?? text),
172
- blocks: options.blocks,
173
- })
174
- : createTextMessage("user", text);
175
- this.history.push(userMessage);
176
- this.sessionStore?.recordMessage(userMessage);
177
- this.scheduleSessionTitleCheck();
240
+ if (text !== undefined) {
241
+ const userMessage = options.blocks
242
+ ? await this.persistMessageImages({
243
+ ...createTextMessage("user", options.displayText ?? text),
244
+ blocks: options.blocks,
245
+ })
246
+ : createTextMessage("user", text);
247
+ this.history.push(userMessage);
248
+ this.sessionStore?.recordMessage(userMessage);
249
+ this.scheduleSessionTitleCheck();
250
+ }
178
251
  const initMessage = createSystemInitMessage({
179
252
  agentId: this.agentId,
180
- tools: this.options.tools.names(),
253
+ tools: (this.simulation?.tools ?? this.options.tools).names(),
181
254
  model: this.currentModel,
182
255
  commands: [...(this.options.commands ?? [])],
183
256
  agents: [...(this.options.agents ?? [])],
@@ -193,6 +266,7 @@ export class QueryEngine {
193
266
  message: initMessage,
194
267
  };
195
268
  this.sessionStore?.recordMessage(initMessage);
269
+ const compactor = this.cancellableCompactor(options.abortSignal);
196
270
  const queryOptions = {
197
271
  agentId: this.agentId,
198
272
  model: this.currentModel,
@@ -220,29 +294,33 @@ export class QueryEngine {
220
294
  } : undefined,
221
295
  abortSignal: options.abortSignal,
222
296
  stopAfterTurn: options.stopAfterTurn,
223
- beforeTurn: (messages) => {
224
- this.options.refreshTools?.(this);
225
- return this.consumeSessionSettings(messages);
297
+ beforeTurn: (messages, started) => {
298
+ if (!this.simulation)
299
+ this.options.refreshTools?.(this);
300
+ return this.consumeSessionSettings(messages, started, compactor);
226
301
  },
227
302
  };
228
303
  const stream = query(this.history, {
229
304
  ...this.options,
230
- acquireTurnResources: this.options.acquireTurnResources ? () => this.options.acquireTurnResources(this) : undefined,
305
+ acquireTurnResources: this.simulation ? undefined : this.options.acquireTurnResources ? () => this.options.acquireTurnResources(this) : undefined,
306
+ tools: this.simulation?.tools ?? this.options.tools,
307
+ compactor,
231
308
  onTimingState: timing => { this.queryTiming = timing; },
232
309
  onTiming: record => {
233
310
  this.timingRecords.set(record.id, { ...record });
234
311
  this.sessionStore?.recordTiming(record);
235
312
  },
236
313
  contextManager: this.contextManager,
237
- modelGateway: this.currentModelGateway,
314
+ modelGateway: this.simulation?.modelGateway ?? this.currentModelGateway,
238
315
  taskNotificationSource: this.options.taskNotificationSource,
316
+ imageStore: this.imageStore,
239
317
  toolResultMemory: this.sessionStore?.toolResultMemory,
240
318
  session: this.sessionStore ? { sessionId: this.sessionStore.sessionId, sessionDir: this.sessionStore.sessionDir, rootDir: this.options.session?.rootDir } : undefined,
241
319
  recordContentReplacements: (records) => this.sessionStore?.recordContentReplacements(records),
242
320
  // query owns synthetic tool-use messages; persist them only via the message event below.
243
321
  // The export callback is an observer, not a second history writer.
244
322
  exportToolCalls: this.options.exportToolCalls,
245
- applyCompaction: (result) => this.applyCompactionResult(result),
323
+ applyCompaction: (result) => { options.abortSignal?.throwIfAborted(); this.applyCompactionResult(result); },
246
324
  }, queryOptions);
247
325
  for await (const event of stream) {
248
326
  if (event.type === "message") {
@@ -253,13 +331,17 @@ export class QueryEngine {
253
331
  continue;
254
332
  }
255
333
  if (event.type === "terminal") {
334
+ terminalReceived = true;
256
335
  this.lastTerminalReason = event.reason;
257
- // The final assistant/tool messages have already been recorded by this wrapper.
258
- const hadPending = Object.keys(this.pendingSessionSettings).length > 0;
259
- for (const result of await this.flushSessionSettings()) {
260
- if (result.report)
261
- yield { type: "context.compacted", compaction: result.report };
336
+ if (this.executionState?.status !== "paused") {
337
+ this.setExecutionState({
338
+ status: event.reason === "completed" || event.reason === "turn_yielded" ? "completed" : "interrupted",
339
+ reason: event.reason,
340
+ });
262
341
  }
342
+ // Settle ordinary settings only; pending compact waits for the next model call.
343
+ const hadPending = Object.keys(this.pendingSessionSettings).length > 0;
344
+ await this.flushSessionSettings();
263
345
  if (hadPending)
264
346
  yield { type: "context.metrics", metrics: await this.contextMetrics() };
265
347
  }
@@ -267,12 +349,12 @@ export class QueryEngine {
267
349
  }
268
350
  }
269
351
  finally {
270
- // Covers initialization errors, aborts, exceptions and consumer generator.return().
271
- // Do not yield in finally: a closing consumer may never resume the generator.
352
+ // Even a failed journal write must release the engine run lock.
272
353
  try {
273
- do {
274
- await this.flushSessionSettings();
275
- } while (Object.keys(this.pendingSessionSettings).length);
354
+ if (queryAccepted && !terminalReceived && this.executionState?.status !== "paused") {
355
+ this.setExecutionState({ status: "interrupted", reason: "unexpected_termination" });
356
+ }
357
+ await this.flushSessionSettings();
276
358
  }
277
359
  finally {
278
360
  this.running = false;
@@ -280,7 +362,7 @@ export class QueryEngine {
280
362
  }
281
363
  }
282
364
  /** Queue instance-local settings; no prompt messages or provider-global mutation. */
283
- async updateSessionSettings(patch) {
365
+ async updateSessionSettings(patch, options = {}) {
284
366
  const normalized = {};
285
367
  if (Object.hasOwn(patch, "model"))
286
368
  normalized.model = patch.model?.trim() || undefined;
@@ -302,10 +384,26 @@ export class QueryEngine {
302
384
  }
303
385
  const mutation = this.settingsMutation.then(async () => {
304
386
  await this.initialize();
305
- const update = await this.applySessionSettings(normalized, this.getHistoryMessages());
306
- if (update?.compaction)
307
- this.applyCompactionResult(update.compaction);
308
- return { deferred: false, ...(update?.compaction ? { compaction: update.compaction } : {}) };
387
+ if (normalized.compact)
388
+ delete this.pendingSessionSettings.compact;
389
+ const previousExecution = this.getExecutionState();
390
+ if (normalized.compact)
391
+ this.setExecutionState({ status: "running", resumeCompaction: true });
392
+ try {
393
+ const update = await this.applySessionSettings(normalized, this.getHistoryMessages(), undefined, this.cancellableCompactor(options.abortSignal));
394
+ options.abortSignal?.throwIfAborted();
395
+ if (update?.compaction)
396
+ this.applyCompactionResult(update.compaction);
397
+ if (normalized.compact)
398
+ this.setExecutionState({ status: previousExecution?.status ?? "completed", reason: previousExecution?.reason });
399
+ return { deferred: false, ...(update?.compaction ? { compaction: update.compaction } : {}) };
400
+ }
401
+ catch (error) {
402
+ if (normalized.compact)
403
+ this.setExecutionState(options.abortSignal?.aborted
404
+ ? { status: "paused", reason: "user_pause" } : { status: "interrupted", reason: "compaction_failed" });
405
+ throw error;
406
+ }
309
407
  });
310
408
  // A rejected mutation must not poison subsequent settings or user input.
311
409
  this.settingsMutation = mutation.catch(() => undefined);
@@ -357,14 +455,14 @@ export class QueryEngine {
357
455
  patch.sessionPrompt = { ...patch.sessionPrompt };
358
456
  return patch;
359
457
  }
360
- async consumeSessionSettings(messages) {
361
- const patch = this.pendingSessionSettings;
458
+ async consumeSessionSettings(messages, started, compactor) {
459
+ const patch = { ...this.pendingSessionSettings, ...(this.executionState?.resumeCompaction ? { compact: true } : {}) };
362
460
  if (!Object.keys(patch).length)
363
461
  return undefined;
364
462
  this.pendingSessionSettings = {};
365
- return this.applySessionSettings(patch, messages);
463
+ return this.applySessionSettings(patch, messages, started, compactor);
366
464
  }
367
- async applySessionSettings(patch, messages) {
465
+ async applySessionSettings(patch, messages, started, activeCompactor) {
368
466
  const changed = {};
369
467
  if (Object.hasOwn(patch, "sessionPrompt"))
370
468
  this.activeSessionPrompt = patch.sessionPrompt ?? null;
@@ -388,23 +486,20 @@ export class QueryEngine {
388
486
  }
389
487
  let compaction;
390
488
  if (patch.compact) {
391
- const compactor = this.options.compactor ?? new ModelDrivenCompactor(this.currentModelGateway);
392
- const budget = { ...this.options.contextBudget, contextWindowTokens: this.currentContextWindowTokens ?? this.options.contextBudget?.contextWindowTokens };
489
+ const compactor = activeCompactor ?? this.cancellableCompactor();
490
+ const budget = { ...this.options.contextBudget, onCompactionStart: started, imageRegistry: this.imageStore?.snapshot(), contextWindowTokens: this.currentContextWindowTokens ?? this.options.contextBudget?.contextWindowTokens };
393
491
  compaction = withCompactionReport(await (compactor.manualCompact?.(messages, budget) ?? compactor.compact(messages, budget)), messages.length);
394
492
  }
395
493
  return Object.keys(changed).length || compaction ? { settings: changed, compaction } : undefined;
396
494
  }
397
495
  async flushSessionSettings() {
398
- const results = [];
399
- while (Object.keys(this.pendingSessionSettings).length) {
400
- const update = await this.consumeSessionSettings(this.getHistoryMessages());
401
- if (update?.compaction) {
402
- this.applyCompactionResult(update.compaction);
403
- if (update.compaction.changed)
404
- results.push(update.compaction);
405
- }
496
+ // Ordinary settings can settle at the terminal boundary. A busy compact request
497
+ // must wait for the next model call, including when the current query ends/aborts.
498
+ while (Object.keys(this.pendingSessionSettings).some(key => key !== "compact")) {
499
+ const { compact, ...patch } = this.pendingSessionSettings;
500
+ this.pendingSessionSettings = compact ? { compact: true } : {};
501
+ await this.applySessionSettings(patch, this.getHistoryMessages());
406
502
  }
407
- return results;
408
503
  }
409
504
  setModel(model, reasoning, updateReasoning = false) {
410
505
  this.currentModel = model?.trim() || undefined;
@@ -464,9 +559,13 @@ export class QueryEngine {
464
559
  reset() {
465
560
  if (this.running)
466
561
  throw new Error("cannot reset while query engine is running");
562
+ this.simulation = undefined;
563
+ this.displayOnlyMessages.length = 0;
467
564
  this.pendingSessionSettings = {};
468
565
  this.history.length = 0;
469
566
  this.lastTerminalReason = undefined;
567
+ this.executionState = undefined;
568
+ this.executionStartedAt = undefined;
470
569
  this.cancelPendingTitleWork();
471
570
  this.sessionStore?.reset();
472
571
  this.queryTiming = undefined;
@@ -476,16 +575,17 @@ export class QueryEngine {
476
575
  async compact(options = {}) {
477
576
  if (options.abortSignal?.aborted)
478
577
  return { messages: this.getHistoryMessages(), changed: false, reason: "none" };
479
- const result = await this.updateSessionSettings({ compact: true });
578
+ const result = await this.updateSessionSettings({ compact: true }, options);
480
579
  return result.compaction ?? { messages: this.getHistoryMessages(), changed: false, reason: "none" };
481
580
  }
482
581
  async pureCompact(options = {}) {
483
582
  await this.initialize();
484
583
  if (options.abortSignal?.aborted)
485
584
  return { messages: this.getHistoryMessages(), changed: false, reason: "none" };
486
- const compactor = this.options.compactor ?? new ModelDrivenCompactor(this.currentModelGateway);
585
+ const compactor = this.simulation?.compactor ?? this.options.compactor ?? new ModelDrivenCompactor(this.currentModelGateway);
487
586
  const contextBudget = {
488
587
  ...this.options.contextBudget,
588
+ imageRegistry: this.imageStore?.snapshot(),
489
589
  contextWindowTokens: this.currentContextWindowTokens ?? this.options.contextBudget?.contextWindowTokens,
490
590
  };
491
591
  const result = withCompactionReport(await (compactor.pureCompact?.(this.history, contextBudget) ?? { messages: this.getHistoryMessages(), changed: false, reason: "none" }), this.history.length);
@@ -521,12 +621,44 @@ export class QueryEngine {
521
621
  return [...records.values()].map(record => ({ ...record }));
522
622
  }
523
623
  getDisplayEntries() {
524
- return this.sessionStore?.getDisplayEntries() ?? this.history.map((message) => ({ type: "message", message: cloneMessage(message) }));
624
+ return this.sessionStore?.getDisplayEntries() ?? [...this.history, ...this.displayOnlyMessages].map((message) => ({ type: "message", message: cloneMessage(message) }));
625
+ }
626
+ clearPendingCompaction() {
627
+ if (!this.executionState?.resumeCompaction)
628
+ return;
629
+ delete this.executionState.resumeCompaction;
630
+ this.setExecutionState(this.getExecutionState());
631
+ }
632
+ /** Keep the durable history untouched until the entire result can be committed. */
633
+ cancellableCompactor(signal) {
634
+ const source = this.simulation?.compactor ?? this.options.compactor ?? new ModelDrivenCompactor(this.currentModelGateway);
635
+ const run = (messages, budget = {}, kind = "compact", error) => {
636
+ const started = reason => {
637
+ signal?.throwIfAborted();
638
+ if (!this.executionState?.resumeCompaction) {
639
+ this.setExecutionState({ ...(this.getExecutionState() ?? { status: "running" }), resumeCompaction: true });
640
+ }
641
+ budget.onCompactionStart?.(reason);
642
+ };
643
+ return abortableWork(async () => {
644
+ if (kind !== "compact")
645
+ started(kind === "manualCompact" ? "manualcompact" : "reactive_compact");
646
+ const options = { ...budget, abortSignal: signal, onCompactionStart: started };
647
+ return kind === "reactiveCompact" && source.reactiveCompact ? source.reactiveCompact(messages, error, options)
648
+ : kind === "manualCompact" && source.manualCompact ? source.manualCompact(messages, options)
649
+ : source.compact(messages, options);
650
+ }, signal);
651
+ };
652
+ return { compact: (m, b) => run(m, b), manualCompact: (m, b) => run(m, b, "manualCompact"),
653
+ reactiveCompact: (m, e, b) => run(m, b, "reactiveCompact", e) };
525
654
  }
526
655
  applyCompactionResult(result) {
527
- if (!result.changed)
656
+ if (!result.changed) {
657
+ this.clearPendingCompaction();
528
658
  return;
659
+ }
529
660
  this.sessionStore?.recordCompactCheckpoint(result.messages, result.reason, result.report);
661
+ this.clearPendingCompaction();
530
662
  this.history.length = 0;
531
663
  this.history.push(...result.messages.map(cloneMessage));
532
664
  }
@@ -559,14 +691,14 @@ export class QueryEngine {
559
691
  return this.buildPromptExportSnapshot(this.getHistoryMessages());
560
692
  }
561
693
  refreshToolsIfIdle() {
562
- if (!this.running)
694
+ if (!this.running && !this.simulation)
563
695
  this.options.refreshTools?.(this);
564
696
  }
565
697
  async buildPromptExportSnapshot(messages, desiredState = this.sessionPromptState) {
566
698
  this.refreshToolsIfIdle();
567
699
  const toolContext = {
568
700
  agentId: this.agentId,
569
- tools: this.options.tools,
701
+ tools: this.simulation?.tools ?? this.options.tools,
570
702
  appState: new InMemoryAppState(this.agentId, this.options.cwd),
571
703
  toolResultMemory: this.sessionStore?.toolResultMemory,
572
704
  session: this.sessionStore ? {
@@ -585,7 +717,7 @@ export class QueryEngine {
585
717
  },
586
718
  emit: () => undefined,
587
719
  };
588
- const initialToolDefinitions = this.options.tools.definitions(toolContext);
720
+ const initialToolDefinitions = (this.simulation?.tools ?? this.options.tools).definitions(toolContext);
589
721
  const activePrompt = this.activeSessionPrompt;
590
722
  const baseline = await this.baselineContextManager.build({
591
723
  agentId: this.agentId,
@@ -630,24 +762,15 @@ export class QueryEngine {
630
762
  };
631
763
  }
632
764
  async persistMessageImages(message) {
633
- if (!message.blocks.some((block) => block.type === "image"))
634
- return message;
635
- const persistedBlocks = await persistMessageImages(message.blocks, { sessionDir: this.sessionStore?.sessionDir, agentId: this.agentId });
636
- const hasRetention = typeof message.metadata?.imageRetention === "string";
637
- return {
638
- ...message,
639
- blocks: persistedBlocks,
640
- metadata: message.role === "user"
641
- ? { ...message.metadata, imageRetention: "pinned", imageTtlTurns: undefined }
642
- : hasRetention
643
- ? message.metadata
644
- : { ...message.metadata, imageRetention: "while_relevant", imageTtlTurns: 4 },
645
- };
765
+ this.imageStore ??= await SessionImageStore.open(this.sessionStore?.sessionDir, this.agentId);
766
+ return this.imageStore.persistMessage(message);
646
767
  }
647
768
  async openSession(options) {
648
769
  if (this.options.session?.enabled === false || !this.options.session)
649
770
  return;
650
771
  this.cancelPendingTitleWork();
772
+ this.simulation = undefined;
773
+ this.displayOnlyMessages.length = 0;
651
774
  this.sessionStore = await SessionStore.open({
652
775
  agentId: this.agentId,
653
776
  cwd: this.options.cwd ?? process.cwd(),
@@ -656,6 +779,11 @@ export class QueryEngine {
656
779
  resume: options.resume,
657
780
  toolResultThresholdChars: this.options.session.toolResultThresholdChars,
658
781
  });
782
+ this.imageStore = await SessionImageStore.open(this.sessionStore.sessionDir, this.agentId);
783
+ const execution = this.sessionStore.getExecutionState();
784
+ this.executionState = execution?.status === "running"
785
+ ? { ...execution, status: "interrupted", reason: "runtime_terminated" } : execution;
786
+ this.executionStartedAt = undefined;
659
787
  this.pendingSessionSettings = {};
660
788
  this.sessionPromptState = this.sessionStore.getSessionPrompt();
661
789
  this.activeSessionPrompt = this.sessionPromptState ?? null;
@@ -674,7 +802,7 @@ export class QueryEngine {
674
802
  }
675
803
  scheduleSessionTitleCheck() {
676
804
  const store = this.sessionStore;
677
- if (!store || titleWorkComplete(store))
805
+ if (this.simulation || !store || titleWorkComplete(store))
678
806
  return;
679
807
  const version = this.titleSchedulerVersion;
680
808
  const timer = setTimeout(() => {
@@ -689,7 +817,7 @@ export class QueryEngine {
689
817
  if (version !== this.titleSchedulerVersion)
690
818
  return;
691
819
  const store = this.sessionStore;
692
- if (!store || titleWorkComplete(store))
820
+ if (this.simulation || !store || titleWorkComplete(store))
693
821
  return;
694
822
  if (this.titleAgentRun?.version === version)
695
823
  return;