tinker-agent 2.10.0 → 2.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/CHANGELOG.md +47 -1
  2. package/README.md +44 -67
  3. package/package.json +4 -3
  4. package/src/agent/loop.ts +50 -13
  5. package/src/agent/runtime-provider-retry.ts +115 -0
  6. package/src/agent/runtime-session-contracts.ts +13 -29
  7. package/src/agent/runtime-session.ts +42 -67
  8. package/src/cli/config.ts +0 -29
  9. package/src/cli/model-profiles.ts +0 -80
  10. package/src/cli/public-config-contract.ts +1 -82
  11. package/src/cli/tui-runner.tsx +3 -41
  12. package/src/events/observation-text-log.ts +7 -0
  13. package/src/events/types.ts +8 -0
  14. package/src/image/abortable-file-open.ts +54 -0
  15. package/src/image/image-asset-store.ts +7 -1
  16. package/src/memory/contracts.ts +0 -256
  17. package/src/memory/memory-create-tool.ts +3 -5
  18. package/src/memory/memory-files.ts +145 -0
  19. package/src/memory/memory-search-output.ts +23 -0
  20. package/src/memory/memory-search-tool.ts +79 -175
  21. package/src/memory/memory-search.ts +115 -0
  22. package/src/model/fake-model-client.ts +20 -1
  23. package/src/model/openai-model-utils.ts +45 -1
  24. package/src/model/openai-responses-mapping.ts +11 -0
  25. package/src/model/openai-responses-stream.ts +14 -0
  26. package/src/observation/observation-builder.ts +5 -1
  27. package/src/session/scoped-query-database.ts +27 -0
  28. package/src/session/session-history-access.ts +4 -3
  29. package/src/session/session-store-contracts.ts +0 -23
  30. package/src/session/session-store.ts +9 -106
  31. package/src/tools/grep.ts +12 -4
  32. package/src/tools/registry.ts +15 -18
  33. package/src/tools/types.ts +12 -0
  34. package/src/tui/app.tsx +51 -7
  35. package/src/tui/components/ask-user.tsx +15 -8
  36. package/src/tui/components/prompt-input.tsx +14 -6
  37. package/src/tui/components/timeline.tsx +17 -9
  38. package/src/tui/event-store.ts +13 -1
  39. package/src/tui/file-mention.ts +29 -5
  40. package/src/tui/tui-projection-store.ts +5 -2
  41. package/src/tui/tui-session-controller.ts +8 -0
  42. package/src/tui/workspace-file-search.ts +21 -0
  43. package/src/cli/tui-memory.ts +0 -69
  44. package/src/memory/embedding-client.ts +0 -105
  45. package/src/memory/memory-coordinator.ts +0 -1274
  46. package/src/memory/memory-delete-tool.ts +0 -88
  47. package/src/memory/memory-extractor.ts +0 -252
  48. package/src/memory/memory-get-tool.ts +0 -86
  49. package/src/memory/memory-log.ts +0 -88
  50. package/src/memory/memory-store.ts +0 -1133
  51. package/src/memory/memory-update-tool.ts +0 -142
  52. package/src/memory/vector.ts +0 -153
@@ -1,3 +1,8 @@
1
+ import { prepareSessionMemory, sessionMemoryPath } from "../memory/memory-files";
2
+ import {
3
+ RuntimeProviderRetry,
4
+ type ProviderRetryDecision,
5
+ } from "./runtime-provider-retry";
1
6
  import path from "node:path";
2
7
  import { assertContextMaintenanceCapabilities } from "./runtime-context-capabilities";
3
8
  import { CompiledContextError } from "../context/compiled-context-validator";
@@ -21,7 +26,7 @@ import type { EventSink } from "../events/event-sink";
21
26
  import { JsonlEventLog } from "../events/jsonl-event-log";
22
27
  import { ObservationTextLog } from "../events/observation-text-log";
23
28
  import type { AgentEvent, AgentEventInput } from "../events/types";
24
- import { runtimeIdFactory, type SessionId, type TurnId } from "../ids/runtime-id";
29
+ import { runtimeIdFactory, type SessionId } from "../ids/runtime-id";
25
30
  import { ImageAssetStore, type ImportedImageAsset } from "../image/image-asset-store";
26
31
  import { IMAGE_INPUT_POLICY } from "../image/image-input-policy";
27
32
  import {
@@ -48,7 +53,6 @@ import { SessionError } from "../session/session-errors";
48
53
  import {
49
54
  createSessionCompatibilityContract,
50
55
  SessionStore,
51
- type CompletedTurnSnapshot,
52
56
  type SessionRecoveryResult,
53
57
  type StoredSkillActivation,
54
58
  } from "../session/session-store";
@@ -79,8 +83,6 @@ import {
79
83
  type AskUserResolution,
80
84
  type AskUserSnapshot,
81
85
  type BashGuardSnapshot,
82
- type CompletedTurnHook,
83
- type CompletedTurnHookFailure,
84
86
  type CreateNewRuntimeSessionInput,
85
87
  type CreateRuntimeSessionInput,
86
88
  type ExecuteTurnInput,
@@ -119,9 +121,6 @@ export {
119
121
  type AskUserSnapshot,
120
122
  type BashGuardSnapshot,
121
123
  type BashGuardSource,
122
- type CompletedTurnHook,
123
- type CompletedTurnHookFailure,
124
- type CompletedTurnHookInput,
125
124
  type ContextSurfaceRefreshSummary,
126
125
  type CreateRuntimeSessionInput,
127
126
  type ExecuteTurnInput,
@@ -221,6 +220,7 @@ class DefaultRuntimeSession implements RuntimeSession {
221
220
 
222
221
  private readonly skillCatalog: SkillCatalogSnapshot;
223
222
  private readonly interactions: RuntimeInteractions;
223
+ private readonly providerRetryInteraction: RuntimeProviderRetry;
224
224
  private readonly scheduler: RuntimePromptScheduler;
225
225
  private readonly contextMaintenance: RuntimeContextMaintenance;
226
226
  private readonly runtimeSkills: RuntimeSkills;
@@ -234,6 +234,9 @@ class DefaultRuntimeSession implements RuntimeSession {
234
234
  private readonly store: SessionStore,
235
235
  private readonly assetStore: ImageAssetStore,
236
236
  ) {
237
+ this.providerRetryInteraction = new RuntimeProviderRetry((event) =>
238
+ this.append(event),
239
+ );
237
240
  this.sessionId = input.selection.sessionId;
238
241
  this.resumed = input.selection.mode === "resume";
239
242
  this.scheduler = new RuntimePromptScheduler(
@@ -339,6 +342,16 @@ class DefaultRuntimeSession implements RuntimeSession {
339
342
  }
340
343
  let session: DefaultRuntimeSession;
341
344
  try {
345
+ if (
346
+ input.persistence !== false &&
347
+ input.persistence?.observationLogPath === undefined
348
+ ) {
349
+ await prepareSessionMemory(
350
+ store.sessionDirectory,
351
+ input.selection.sessionId,
352
+ input.homeRoot,
353
+ );
354
+ }
342
355
  session = new DefaultRuntimeSession(
343
356
  input,
344
357
  dependencies,
@@ -466,16 +479,9 @@ class DefaultRuntimeSession implements RuntimeSession {
466
479
  ...(input.memorySearch === undefined
467
480
  ? {}
468
481
  : { memorySearch: input.memorySearch }),
469
- ...(input.memoryGet === undefined ? {} : { memoryGet: input.memoryGet }),
470
482
  ...(input.memoryCreate === undefined
471
483
  ? {}
472
484
  : { memoryCreate: input.memoryCreate }),
473
- ...(input.memoryUpdate === undefined
474
- ? {}
475
- : { memoryUpdate: input.memoryUpdate }),
476
- ...(input.memoryDelete === undefined
477
- ? {}
478
- : { memoryDelete: input.memoryDelete }),
479
485
  ...(session.skillCatalog.skills.size === 0
480
486
  ? {}
481
487
  : {
@@ -694,6 +700,21 @@ class DefaultRuntimeSession implements RuntimeSession {
694
700
  return this.interactions.resolveBashConfirmation(decision);
695
701
  }
696
702
 
703
+ providerRetry() {
704
+ return this.providerRetryInteraction.read();
705
+ }
706
+
707
+ subscribeProviderRetry(listener: () => void): () => void {
708
+ return this.providerRetryInteraction.subscribe(listener);
709
+ }
710
+
711
+ resolveProviderRetry(
712
+ requestId: string,
713
+ decision: ProviderRetryDecision,
714
+ ): Promise<void> {
715
+ return this.providerRetryInteraction.resolve(requestId, decision);
716
+ }
717
+
697
718
  askUser(): AskUserSnapshot {
698
719
  return this.interactions.askUser();
699
720
  }
@@ -1184,6 +1205,12 @@ class DefaultRuntimeSession implements RuntimeSession {
1184
1205
  signal,
1185
1206
  assetStore: this.assetStore,
1186
1207
  initialRequest,
1208
+ ...(this.input.enableProviderRetryPrompt === true
1209
+ ? {
1210
+ requestProviderRetry: (iteration, failure, signal) =>
1211
+ this.providerRetryInteraction.request(iteration, failure, signal),
1212
+ }
1213
+ : {}),
1187
1214
  });
1188
1215
  } catch (error) {
1189
1216
  if (error instanceof RuntimeEventAppendError) {
@@ -1215,9 +1242,6 @@ class DefaultRuntimeSession implements RuntimeSession {
1215
1242
  pendingLedgerTurn.finish(result);
1216
1243
  settled = true;
1217
1244
  this.requireTooling().turnUndoManager?.completeTurn(turn);
1218
- if (result.status === "completed") {
1219
- this.notifyCompletedTurn(turn);
1220
- }
1221
1245
  await this.runtimeSkills.settleClosedTurnSkills();
1222
1246
  if (result.status === "completed") {
1223
1247
  await this.contextMaintenance.evaluateClosedTurnContextPressure();
@@ -1241,55 +1265,6 @@ class DefaultRuntimeSession implements RuntimeSession {
1241
1265
  }
1242
1266
  }
1243
1267
 
1244
- private notifyCompletedTurn(turn: TurnIdentity): void {
1245
- const hook = this.input.completedTurnHook;
1246
- if (hook === undefined) {
1247
- return;
1248
- }
1249
- let snapshot: CompletedTurnSnapshot;
1250
- try {
1251
- snapshot = this.store.readCompletedTurnSnapshot(turn.turnId);
1252
- } catch {
1253
- this.recordCompletedTurnHookFailure(
1254
- hook,
1255
- turn.turnId,
1256
- "completed_turn_snapshot_failed",
1257
- );
1258
- return;
1259
- }
1260
- try {
1261
- hook.enqueue({
1262
- workspaceRoot: this.input.workspaceRoot,
1263
- sessionId: this.sessionId,
1264
- turnId: turn.turnId,
1265
- snapshot,
1266
- });
1267
- } catch {
1268
- this.recordCompletedTurnHookFailure(
1269
- hook,
1270
- turn.turnId,
1271
- "completed_turn_enqueue_failed",
1272
- );
1273
- }
1274
- }
1275
-
1276
- private recordCompletedTurnHookFailure(
1277
- hook: CompletedTurnHook,
1278
- turnId: TurnId,
1279
- reason: CompletedTurnHookFailure["reason"],
1280
- ): void {
1281
- try {
1282
- hook.recordFailure({
1283
- workspaceRoot: this.input.workspaceRoot,
1284
- sessionId: this.sessionId,
1285
- turnId,
1286
- reason,
1287
- });
1288
- } catch {
1289
- // Optional completed-turn integrations never fault a committed turn.
1290
- }
1291
- }
1292
-
1293
1268
  private async appendTerminalEvent(
1294
1269
  turn: TurnIdentity,
1295
1270
  result: RunAgentResult,
@@ -1715,7 +1690,7 @@ function createEventSink(
1715
1690
  ),
1716
1691
  new ObservationTextLog(
1717
1692
  input.persistence?.observationLogPath ??
1718
- path.join(sessionDirectory, "observations.md"),
1693
+ sessionMemoryPath(input.selection.sessionId, input.homeRoot),
1719
1694
  ),
1720
1695
  );
1721
1696
  }
package/src/cli/config.ts CHANGED
@@ -17,7 +17,6 @@ import {
17
17
  type ModelProfiles,
18
18
  unknownProfileError,
19
19
  } from "./model-profiles";
20
- import type { MemoryEmbeddingConfig } from "../memory/contracts";
21
20
  import type { ReasoningEffortConfig } from "../model/reasoning-effort";
22
21
  import {
23
22
  parsePublicEnvironment,
@@ -54,12 +53,6 @@ export type RunnerConfigSelection = {
54
53
 
55
54
  type RunnerConfigTemplate = Omit<RunnerConfig, "sessionId">;
56
55
 
57
- export type ResolvedMemoryConfig = {
58
- readonly profile: ModelProfile;
59
- readonly contextBudget: ModelContextBudget;
60
- readonly embedding: MemoryEmbeddingConfig;
61
- };
62
-
63
56
  export type ResolvedPublicConfig =
64
57
  | {
65
58
  readonly mode: "env";
@@ -71,7 +64,6 @@ export type ResolvedPublicConfig =
71
64
  readonly tooling: PublicToolingConfig;
72
65
  readonly profiles: ModelProfiles;
73
66
  readonly templates: ReadonlyMap<string, RunnerConfigTemplate>;
74
- readonly memory?: ResolvedMemoryConfig;
75
67
  readonly persistDefaultProfile: (profileName: string) => Promise<void>;
76
68
  };
77
69
 
@@ -101,16 +93,11 @@ export function createResolvedPublicConfig(
101
93
  runnerConfigTemplateFromProfile(environment, profile),
102
94
  ]),
103
95
  );
104
- const memory =
105
- profiles.memory === undefined
106
- ? undefined
107
- : resolveMemoryConfig(profiles, profiles.memory.profile);
108
96
  return Object.freeze({
109
97
  mode: "profile",
110
98
  tooling: environment.tooling,
111
99
  profiles,
112
100
  templates,
113
- ...(memory === undefined ? {} : { memory }),
114
101
  persistDefaultProfile: (profileName: string) =>
115
102
  persistDefaultProfile(profileName, environment.modelsPath),
116
103
  });
@@ -126,22 +113,6 @@ export function createResolvedPublicConfig(
126
113
  });
127
114
  }
128
115
 
129
- function resolveMemoryConfig(
130
- profiles: ModelProfiles,
131
- profileName: string,
132
- ): ResolvedMemoryConfig {
133
- const profile = profiles.profiles.get(profileName);
134
- if (profile === undefined || profiles.memory === undefined) {
135
- throw unknownProfileError(profileName, profiles);
136
- }
137
- const contextProfile = profileToContextProfile(profile);
138
- return Object.freeze({
139
- profile,
140
- contextBudget: deriveModelContextBudget(contextProfile),
141
- embedding: profiles.memory.embedding,
142
- });
143
- }
144
-
145
116
  export function deriveRunnerConfig(
146
117
  snapshot: ResolvedPublicConfig,
147
118
  selection: RunnerConfigSelection,
@@ -5,13 +5,10 @@ import {
5
5
  } from "../model/model-context-profile";
6
6
  import { parseModelApi, type ModelApi } from "../model/model-api";
7
7
  import {
8
- MEMORY_CONFIG_FIELDS,
9
- MEMORY_EMBEDDING_FIELDS,
10
8
  MODEL_PROFILE_FIELDS,
11
9
  MODEL_PROFILES_DOCUMENT_FIELDS,
12
10
  MODEL_REASONING_FIELDS,
13
11
  } from "./public-config-contract";
14
- import type { MemoryEmbeddingConfig } from "../memory/contracts";
15
12
  import type { ReasoningEffortConfig } from "../model/reasoning-effort";
16
13
  import type { ModelInputModality, ToolResultModality } from "../model/model-client";
17
14
 
@@ -34,12 +31,6 @@ export type { ModelInputModality, ToolResultModality } from "../model/model-clie
34
31
  export type ModelProfiles = {
35
32
  readonly defaultProfile: string;
36
33
  readonly profiles: ReadonlyMap<string, ModelProfile>;
37
- readonly memory?: MemoryConfig;
38
- };
39
-
40
- export type MemoryConfig = {
41
- readonly profile: string;
42
- readonly embedding: MemoryEmbeddingConfig;
43
34
  };
44
35
 
45
36
  export async function loadModelProfiles(configPath: string): Promise<ModelProfiles> {
@@ -146,14 +137,9 @@ export function parseModelProfiles(raw: string, sourcePath: string): ModelProfil
146
137
  profiles.set(profileName, parseProfile(profileName, profileValue, sourcePath));
147
138
  }
148
139
 
149
- const memory =
150
- json.memory === undefined
151
- ? undefined
152
- : parseMemoryConfig(json.memory, profiles, sourcePath);
153
140
  return Object.freeze({
154
141
  defaultProfile,
155
142
  profiles,
156
- ...(memory === undefined ? {} : { memory }),
157
143
  });
158
144
  }
159
145
 
@@ -370,56 +356,6 @@ function parseReasoning(value: unknown, where: string): ReasoningEffortConfig {
370
356
  });
371
357
  }
372
358
 
373
- function parseMemoryConfig(
374
- value: unknown,
375
- profiles: ReadonlyMap<string, ModelProfile>,
376
- sourcePath: string,
377
- ): MemoryConfig {
378
- const where = `Model profiles ${sourcePath}: "memory"`;
379
- if (!isRecord(value)) {
380
- throw new Error(`${where} must be an object.`);
381
- }
382
- assertKnownKeys(
383
- value,
384
- MEMORY_CONFIG_FIELDS.map((field) => field.name),
385
- where,
386
- );
387
- const profile = requireString(value.profile, `${where}.profile`);
388
- if (!profiles.has(profile)) {
389
- throw unknownProfileNamesError(profile, [...profiles.keys()]);
390
- }
391
- const embedding = parseMemoryEmbedding(value.embedding, `${where}.embedding`);
392
- return Object.freeze({ profile, embedding });
393
- }
394
-
395
- function parseMemoryEmbedding(value: unknown, where: string): MemoryEmbeddingConfig {
396
- if (!isRecord(value)) {
397
- throw new Error(`${where} must be an object.`);
398
- }
399
- assertKnownKeys(
400
- value,
401
- MEMORY_EMBEDDING_FIELDS.map((field) => field.name),
402
- where,
403
- );
404
- const name = requireString(value.name, `${where}.name`);
405
- if (value.kind !== "openai-compatible") {
406
- throw new Error(`${where}.kind must be "openai-compatible".`);
407
- }
408
- const model = requireString(value.model, `${where}.model`);
409
- const apiBase = requireString(value.apiBase, `${where}.apiBase`);
410
- requireHttpUrl(apiBase, `${where}.apiBase`);
411
- const apiKey = requireString(value.apiKey, `${where}.apiKey`);
412
- const dimensions = requirePositiveInteger(value.dimensions, `${where}.dimensions`);
413
- return Object.freeze({
414
- name,
415
- kind: "openai-compatible",
416
- model,
417
- apiBase,
418
- apiKey,
419
- dimensions,
420
- });
421
- }
422
-
423
359
  function parseInputModalities(
424
360
  value: unknown,
425
361
  name: string,
@@ -538,22 +474,6 @@ function requirePositiveInteger(value: unknown, name: string): number {
538
474
  return value;
539
475
  }
540
476
 
541
- function requireHttpUrl(value: string, name: string): void {
542
- let parsed: URL;
543
- try {
544
- parsed = new URL(value);
545
- } catch {
546
- throw new Error(`${name} must be a valid HTTP(S) URL.`);
547
- }
548
- if (
549
- (parsed.protocol !== "http:" && parsed.protocol !== "https:") ||
550
- parsed.username !== "" ||
551
- parsed.password !== ""
552
- ) {
553
- throw new Error(`${name} must be a valid HTTP(S) URL.`);
554
- }
555
- }
556
-
557
477
  function parseBoolean(value: unknown, name: string): boolean {
558
478
  if (typeof value === "boolean") {
559
479
  return value;
@@ -379,91 +379,10 @@ export const MODEL_REASONING_FIELDS = Object.freeze([
379
379
  }),
380
380
  ]);
381
381
 
382
- export type MemoryConfigField = {
383
- readonly name: "profile" | "embedding";
384
- readonly valueKind: "non-empty-string" | "embedding-profile";
385
- readonly required: true;
386
- readonly secret: boolean;
387
- readonly description: string;
388
- };
389
-
390
- export const MEMORY_CONFIG_FIELDS = Object.freeze([
391
- Object.freeze({
392
- name: "profile",
393
- valueKind: "non-empty-string",
394
- required: true,
395
- secret: false,
396
- description:
397
- "Existing model profile used for completed-turn atomic-memory extraction.",
398
- }),
399
- Object.freeze({
400
- name: "embedding",
401
- valueKind: "embedding-profile",
402
- required: true,
403
- secret: true,
404
- description: "Single embedding profile for the global memory database.",
405
- }),
406
- ] satisfies readonly MemoryConfigField[]);
407
-
408
- export type MemoryEmbeddingField = {
409
- readonly name: "name" | "kind" | "model" | "apiBase" | "apiKey" | "dimensions";
410
- readonly valueKind: "non-empty-string" | "positive-integer" | "literal-string";
411
- readonly required: true;
412
- readonly secret: boolean;
413
- readonly literalValue?: "openai-compatible";
414
- readonly description: string;
415
- };
416
-
417
- export const MEMORY_EMBEDDING_FIELDS = Object.freeze([
418
- Object.freeze({
419
- name: "name",
420
- valueKind: "non-empty-string",
421
- required: true,
422
- secret: false,
423
- description: "Stable identity for the embedding space.",
424
- }),
425
- Object.freeze({
426
- name: "kind",
427
- valueKind: "literal-string",
428
- required: true,
429
- secret: false,
430
- literalValue: "openai-compatible",
431
- description: "Embedding transport kind.",
432
- }),
433
- Object.freeze({
434
- name: "model",
435
- valueKind: "non-empty-string",
436
- required: true,
437
- secret: false,
438
- description: "Embedding provider model name.",
439
- }),
440
- Object.freeze({
441
- name: "apiBase",
442
- valueKind: "non-empty-string",
443
- required: true,
444
- secret: false,
445
- description: "OpenAI-compatible API base URL.",
446
- }),
447
- Object.freeze({
448
- name: "apiKey",
449
- valueKind: "non-empty-string",
450
- required: true,
451
- secret: true,
452
- description: "Embedding provider credential.",
453
- }),
454
- Object.freeze({
455
- name: "dimensions",
456
- valueKind: "positive-integer",
457
- required: true,
458
- secret: false,
459
- description: "Fixed vector dimensions for the global memory database.",
460
- }),
461
- ] satisfies readonly MemoryEmbeddingField[]);
462
-
463
382
  export const MODEL_PROFILES_DOCUMENT_FIELDS = Object.freeze([
464
383
  Object.freeze({ name: "default", valueKind: "non-empty-string" as const }),
465
384
  Object.freeze({ name: "profiles", valueKind: "profiles-map" as const }),
466
- Object.freeze({ name: "memory", valueKind: "memory-config" as const }),
385
+ Object.freeze({ name: "memory", valueKind: "ignored-legacy-config" as const }),
467
386
  ]);
468
387
 
469
388
  export type PublicToolingConfig = {
@@ -46,7 +46,7 @@ import { loadSkillCatalog } from "../skills/skill-loader";
46
46
  import { loadProjectSlashCommands } from "../tui/project-slash-commands";
47
47
  import { createWorkspaceFileLister } from "../tui/workspace-file-search";
48
48
  import { clipboardWriterForEnvironment } from "../tui/clipboard";
49
- import { initializeTuiMemory } from "./tui-memory";
49
+ import { listMemoryFiles } from "../memory/memory-files";
50
50
  import { prepareShikiHighlighter } from "../tui/shiki-highlighter";
51
51
  import { createReasoningEffortController } from "../model/reasoning-effort";
52
52
 
@@ -63,13 +63,6 @@ export async function runTui(options: RunTuiOptions): Promise<void> {
63
63
  options.publicConfig.mode === "profile" ? options.publicConfig.profiles : undefined;
64
64
  const config = options.initialRunnerConfig;
65
65
  const workspaceRoot = await realpath(config.workspaceRoot);
66
- const memory = await initializeTuiMemory({
67
- config:
68
- options.publicConfig.mode === "profile" ? options.publicConfig.memory : undefined,
69
- env: options.env,
70
- });
71
- const memoryCoordinator = memory.coordinator;
72
- const memoryNotice = memory.notice;
73
66
  let controller: DefaultTuiSessionController | undefined;
74
67
  let instance: ReturnType<typeof render> | undefined;
75
68
  let disposeReason: SessionDisposeReason = { type: "tui_exit" };
@@ -119,36 +112,12 @@ export async function runTui(options: RunTuiOptions): Promise<void> {
119
112
  toolingConfig: options.publicConfig.tooling,
120
113
  enableTurnUndo: true,
121
114
  enableAskUser: true,
115
+ enableProviderRetryPrompt: true,
122
116
  bashGuard: {
123
117
  mode: sessionConfig.bashGuardMode,
124
118
  source: sessionConfig.bashGuardSource,
125
119
  surface: "tui" as const,
126
120
  },
127
- ...(memoryCoordinator === undefined
128
- ? {}
129
- : {
130
- memorySearch: memoryCoordinator.createSearchToolExecutor({
131
- workspaceRoot,
132
- sessionId,
133
- }),
134
- memoryGet: memoryCoordinator.createGetToolExecutor({
135
- workspaceRoot,
136
- sessionId,
137
- }),
138
- memoryCreate: memoryCoordinator.createCreateToolExecutor({
139
- workspaceRoot,
140
- sessionId,
141
- }),
142
- memoryUpdate: memoryCoordinator.createUpdateToolExecutor({
143
- workspaceRoot,
144
- sessionId,
145
- }),
146
- memoryDelete: memoryCoordinator.createDeleteToolExecutor({
147
- workspaceRoot,
148
- sessionId,
149
- }),
150
- completedTurnHook: memoryCoordinator,
151
- }),
152
121
  };
153
122
  if (mode === "resume") {
154
123
  return createRuntimeSession({
@@ -319,13 +288,7 @@ export async function runTui(options: RunTuiOptions): Promise<void> {
319
288
  onQuit={() => {
320
289
  quitRequested = true;
321
290
  }}
322
- initialNotice={memoryNotice}
323
- memoryDisabledNotice={memoryNotice}
324
- listStoredMemories={
325
- memoryCoordinator === undefined
326
- ? undefined
327
- : () => memoryCoordinator.listStoredMemories()
328
- }
291
+ listStoredMemories={() => listMemoryFiles()}
329
292
  />,
330
293
  { incrementalRendering: true },
331
294
  );
@@ -349,7 +312,6 @@ export async function runTui(options: RunTuiOptions): Promise<void> {
349
312
  );
350
313
  }
351
314
  }
352
- memoryCoordinator?.dispose();
353
315
  }
354
316
 
355
317
  if (primaryError !== undefined) {
@@ -197,6 +197,13 @@ function toolCallSummary(call: ToolCall): string {
197
197
  .join("\n");
198
198
  }
199
199
 
200
+ if (call.name === "MemorySearch" && Array.isArray(args.keywords)) {
201
+ return [
202
+ `Call ID: ${call.toolCallId}`,
203
+ `Keywords: ${JSON.stringify(args.keywords)}`,
204
+ ].join("\n");
205
+ }
206
+
200
207
  const filePath = stringProperty(args, "file_path");
201
208
  const pattern = stringProperty(args, "pattern");
202
209
  return [
@@ -337,6 +337,12 @@ export type AgentEventDataMap = {
337
337
  "agent.iteration.started": { iterationNumber: number };
338
338
  "model.request.started": ModelRequestAttemptData;
339
339
  "model.request.failed": ModelRequestFailedData;
340
+ "model.retry.requested": ModelRequestFailedData;
341
+ "model.retry.resolved": {
342
+ attemptNumber: number;
343
+ decision: "retry" | "stop" | "cancelled";
344
+ durationMs: number;
345
+ };
340
346
  "model.request.finished": ModelRequestAttemptData & {
341
347
  output: ModelRequestOutput;
342
348
  };
@@ -457,6 +463,8 @@ export type AgentEventInput =
457
463
  | "agent.iteration.started"
458
464
  | "model.request.started"
459
465
  | "model.request.failed"
466
+ | "model.retry.requested"
467
+ | "model.retry.resolved"
460
468
  | "model.request.finished"
461
469
  | "context.usage.updated"
462
470
  | "context.shadow.planned"
@@ -0,0 +1,54 @@
1
+ import type { FileHandle } from "node:fs/promises";
2
+
3
+ // An OS permission prompt can leave open() pending without accepting a signal.
4
+ // Cancellation releases the caller; ownership of a late handle stays here.
5
+ export function abortableFileOpen(
6
+ openFile: () => Promise<FileHandle>,
7
+ signal?: AbortSignal,
8
+ onWarning?: (message: string) => void,
9
+ ): Promise<FileHandle> {
10
+ signal?.throwIfAborted();
11
+ if (signal === undefined) return openFile();
12
+
13
+ return new Promise<FileHandle>((resolve, reject) => {
14
+ let cancelled = false;
15
+ const onAbort = () => {
16
+ cancelled = true;
17
+ signal.removeEventListener("abort", onAbort);
18
+ reject(asError(signal.reason));
19
+ };
20
+ signal.addEventListener("abort", onAbort, { once: true });
21
+ // Also handle a synchronous failure from the opener without leaking a listener.
22
+ Promise.resolve()
23
+ .then(() => {
24
+ signal.throwIfAborted();
25
+ return openFile();
26
+ })
27
+ .then(async (handle) => {
28
+ signal.removeEventListener("abort", onAbort);
29
+ if (cancelled) {
30
+ try {
31
+ await handle.close();
32
+ } catch (error) {
33
+ onWarning?.(
34
+ `Failed to close cancelled image file: ${error instanceof Error ? error.message : String(error)}.`,
35
+ );
36
+ }
37
+ } else {
38
+ resolve(handle);
39
+ }
40
+ })
41
+ .catch((error: unknown) => {
42
+ signal.removeEventListener("abort", onAbort);
43
+ if (cancelled) {
44
+ // Late open failures must not become unhandled rejections.
45
+ return;
46
+ }
47
+ reject(asError(error));
48
+ });
49
+ });
50
+ }
51
+
52
+ function asError(error: unknown): Error {
53
+ return error instanceof Error ? error : new Error(String(error), { cause: error });
54
+ }
@@ -13,6 +13,7 @@ import {
13
13
  import path from "node:path";
14
14
  import { createUuidV7 } from "../ids/uuid-v7";
15
15
  import { IMAGE_INPUT_POLICY } from "./image-input-policy";
16
+ import { abortableFileOpen } from "./abortable-file-open";
16
17
  import { probeImageBytes } from "./image-probe";
17
18
  import {
18
19
  normalizeOriginalImageName,
@@ -116,9 +117,14 @@ export class ImageAssetStore {
116
117
  assertContained(this.workspaceRoot, canonicalSource, "Image source realpath");
117
118
  }
118
119
 
119
- const handle = await open(canonicalSource, constants.O_RDONLY | noFollowFlag());
120
+ const handle = await abortableFileOpen(
121
+ () => open(canonicalSource, constants.O_RDONLY | noFollowFlag()),
122
+ options.signal,
123
+ this.onWarning,
124
+ );
120
125
  let bytes: Buffer;
121
126
  try {
127
+ throwIfAborted(options.signal);
122
128
  const handleStat = await handle.stat();
123
129
  if (
124
130
  !handleStat.isFile() ||