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
package/CHANGELOG.md CHANGED
@@ -5,6 +5,50 @@ All notable user-facing changes to Tinker are documented here. The project follo
5
5
 
6
6
  ## [Unreleased]
7
7
 
8
+ ## [2.12.0] - 2026-09-11
9
+
10
+ ### Changed
11
+
12
+ - Replace the vector memory system with plain-text Markdown records. Session
13
+ transcripts are appended to `memory/records/<sessionId>.md` in the global
14
+ Tinker home, and `MemoryCreate` saves explicit Markdown notes. `MemorySearch`
15
+ searches all memory files with literal, case-insensitive keywords and
16
+ paginated, grouped passages; the `MemoryGet`, `MemoryUpdate`, and
17
+ `MemoryDelete` tools and the `memory` model-profile configuration are
18
+ removed. Old memory configuration is ignored, and the existing memory
19
+ database and diagnostic files are moved to `memory-legacy/` on first use,
20
+ preserved but no longer searched. Memory now works in both the TUI and the
21
+ one-shot CLI without any model profile or embedding service.
22
+ - Simplify `MemoryCreate` to a Markdown note with the `text` heading and an
23
+ optional `summary` body, recording creation time and source workspace.
24
+
25
+ ## [2.11.0] - 2026-09-08
26
+
27
+ ### Added
28
+
29
+ - Offer directories in the TUI `@` file-mention popup. Ancestor directories of
30
+ workspace files now appear alongside files, ranked after matching files, and
31
+ selecting a directory inserts its path directly instead of attempting an
32
+ image import.
33
+ - Offer a retry selection in the TUI after provider retries are exhausted, so a
34
+ failed model request can be retried without re-entering the prompt.
35
+
36
+ ### Changed
37
+
38
+ - Cache ESLint results for `check:fast` and split the fast test tier into
39
+ twelve independent shards, shortening local iteration while keeping the
40
+ serial full test run as the final gate.
41
+
42
+ ### Fixed
43
+
44
+ - Align `Grep` context-line precedence and remove an unsupported regex hint
45
+ from its documentation.
46
+ - Warm up the fake ripgrep binary in tests to absorb the macOS first-execution
47
+ validation delay.
48
+ - Cancel pending image file opens without leaking file handles.
49
+ - Emphasize TUI timeline labels and add spacing between messages.
50
+ - Finalize scoped SQLite queries before closing the session database.
51
+
8
52
  ## [2.10.0] - 2026-09-06
9
53
 
10
54
  ### Added
@@ -437,7 +481,9 @@ All notable user-facing changes to Tinker are documented here. The project follo
437
481
  - First formal npm release under the `tinker-agent` package name with the `tinker`
438
482
  executable.
439
483
 
440
- [Unreleased]: https://github.com/ishowshao/tinker/compare/v2.10.0...HEAD
484
+ [Unreleased]: https://github.com/ishowshao/tinker/compare/v2.12.0...HEAD
485
+ [2.12.0]: https://github.com/ishowshao/tinker/releases/tag/v2.12.0
486
+ [2.11.0]: https://github.com/ishowshao/tinker/releases/tag/v2.11.0
441
487
  [2.10.0]: https://github.com/ishowshao/tinker/releases/tag/v2.10.0
442
488
  [2.9.0]: https://github.com/ishowshao/tinker/releases/tag/v2.9.0
443
489
  [2.8.0]: https://github.com/ishowshao/tinker/releases/tag/v2.8.0
package/README.md CHANGED
@@ -34,6 +34,19 @@ This does not pretend that any model has infinite tokens or guarantee that it wi
34
34
  explicit model and context limits. Actual provider support must be established
35
35
  by a qualification matrix; transport compatibility alone is not a guarantee.
36
36
 
37
+ ### Provider retry selection
38
+
39
+ When automatic retries for a provider request are exhausted, the TUI offers
40
+ **Retry again** or **End this turn**. Retrying reuses the same request within the
41
+ same turn and iteration, with a fresh automatic retry allowance. Ending the turn
42
+ (or pressing Esc in the selection) records the original failure. The selection
43
+ adds no conversation message and applies only to the current running process.
44
+ Retryable failures include HTTP 429/500/502/503/504, structured `server_error`
45
+ and `rate_limit_exceeded` errors inside provider streams or failed Responses,
46
+ and transient connection failures, including socket resets during streaming.
47
+ Tool failures and non-retryable errors keep their existing behavior. One-shot
48
+ and remote service runs do not wait for this TUI selection.
49
+
37
50
  ### PTY screen size
38
51
 
39
52
  `Bash` accepts optional `cols` and `rows` for the initial PTY screen size:
@@ -52,7 +65,7 @@ creation; running tasks cannot be resized through these tools.
52
65
 
53
66
  Both Recall tools accept an optional `sessionId` (a canonical session UUID).
54
67
  Omitting it keeps the current-session behavior. In the TUI or one-shot CLI, the
55
- agent can use a session ID you supply or a Memory result's `sourceSessionId`:
68
+ agent can use a session ID you supply or find in a memory record's filename/header:
56
69
 
57
70
  ```ts
58
71
  RecallSearch({ sessionId: sourceSessionId, query: "distinctive-anchor" })
@@ -367,70 +380,6 @@ Image-capable profile example:
367
380
  }
368
381
  ```
369
382
 
370
- The top-level `memory` object is optional. When present, every field below is required. It enables completed-turn extraction and `MemorySearch` only in the TUI; one-shot runs do not load memory.
371
-
372
- | Field | Required | Type / constraint | Secret | Description |
373
- | --- | --- | --- | --- | --- |
374
- | `profile` | Yes | Non-empty string | No | Existing model profile used for completed-turn atomic-memory extraction. |
375
- | `embedding` | Yes | Object | Yes | Single embedding profile for the global memory database. |
376
-
377
- `memory.embedding` fields:
378
-
379
- | Field | Required | Type / constraint | Secret | Description |
380
- | --- | --- | --- | --- | --- |
381
- | `name` | Yes | Non-empty string | No | Stable identity for the embedding space. |
382
- | `kind` | Yes | Literal `"openai-compatible"` | No | Embedding transport kind. |
383
- | `model` | Yes | Non-empty string | No | Embedding provider model name. |
384
- | `apiBase` | Yes | Non-empty string | No | OpenAI-compatible API base URL. |
385
- | `apiKey` | Yes | Non-empty string | Yes | Embedding provider credential. |
386
- | `dimensions` | Yes | Positive integer | No | Fixed vector dimensions for the global memory database. |
387
-
388
- Enabling memory sends completed-turn text (not image bytes) to `memory.profile`, and sends extracted candidates plus search queries to the embedding endpoint. Derived memories are stored in `~/.tinker/memory/memory.sqlite`; newly inserted memory text is appended to the private development log `~/.tinker/memory/extracted-memories.log`.
389
-
390
- Atomic-memory profile example:
391
-
392
- ```json
393
- {
394
- "default": "text",
395
- "profiles": {
396
- "text": {
397
- "model": "example-text-model",
398
- "api": "chat-completions",
399
- "apiBase": "https://api.example.com/v1",
400
- "apiKey": "your-model-api-key",
401
- "contextWindowTokens": 128000,
402
- "maxSupportedOutputTokens": 8192,
403
- "reasoning": {
404
- "supportedEfforts": [
405
- "low",
406
- "medium",
407
- "high"
408
- ],
409
- "defaultEffort": "medium"
410
- },
411
- "includeReasoningContent": false,
412
- "stream": true,
413
- "inputModalities": [
414
- "text"
415
- ],
416
- "toolResultModalities": [
417
- "text"
418
- ]
419
- }
420
- },
421
- "memory": {
422
- "profile": "text",
423
- "embedding": {
424
- "name": "example-embedding-space",
425
- "kind": "openai-compatible",
426
- "model": "example-embedding-model",
427
- "apiBase": "https://embeddings.example.com/v1",
428
- "apiKey": "your-embedding-api-key",
429
- "dimensions": 1024
430
- }
431
- }
432
- }
433
- ```
434
383
  <!-- END GENERATED: MODEL PROFILE FIELDS -->
435
384
 
436
385
  Set `api` to `"responses"` to use the standard Responses API. Keep `apiBase`
@@ -550,6 +499,34 @@ one-shot `tinker run` command. See the
550
499
  [project command loader](src/tui/project-slash-commands.ts) for configuration
551
500
  validation and loading behavior.
552
501
 
502
+ ### Text memory
503
+
504
+ Memory works without a model profile or embedding service in both the TUI and
505
+ one-shot CLI. Session transcripts are appended to
506
+ `<home>/.tinker/memory/records/<sessionId>.md`. `MemoryCreate` saves explicit notes
507
+ to `memory/notes/<id>.md`, using `text` as the heading and optional `summary` as
508
+ the body, with creation time and source workspace.
509
+
510
+ `MemorySearch` searches Markdown under `<home>/.tinker/memory/` using literal,
511
+ case-insensitive keywords. Supply `keywords` (any keyword may match), optionally
512
+ `context` (default 3), `limit` (default 20 matching lines), and `offset` (default 0).
513
+ Each line counts once, even if several keywords match. Keywords cannot span lines.
514
+ Passages are grouped by file with line numbers; `>` marks matching lines and `…`
515
+ separates passages. Long lines retain up to 500 code points around the earliest
516
+ match. Use `Read` with `offset` and `limit` to expand a passage, or `nextOffset` to
517
+ continue searching. There are no regex, glob, output-mode, or multiline parameters.
518
+ Results follow file-path order; changes between calls can affect pagination.
519
+ There are no MemoryGet, MemoryUpdate, or
520
+ MemoryDelete tools. Resume appends to the same record; cloning copies historical
521
+ content; deleting a session leaves its memory record in place. `/memory` browses
522
+ previews of records and notes.
523
+
524
+ Old `models.json` memory configuration is ignored. Existing database and memory
525
+ diagnostic files are moved to `<home>/.tinker/memory-legacy/<id>/` on first use;
526
+ they are preserved but not searched or converted. Older session-local
527
+ `observations.md` files move to records when those sessions are resumed.
528
+ See [the memory design](docs/global-memory/global-memory-design.md).
529
+
553
530
  ### Global Runtime Data
554
531
 
555
532
  Tinker keeps private runtime state in a global home directory instead of inside
@@ -557,8 +534,8 @@ your projects. Each workspace maps to
557
534
  `~/.tinker/projects/<workspace-slug-hash>/` — the slug comes from the workspace
558
535
  directory name and the hash from its canonical absolute path, so one project
559
536
  always resolves to one storage directory even when reached through symlinks.
560
- That directory holds per-session SQLite databases, event logs, and observation
561
- logs (`sessions/<id>/`), background Bash task logs (`bash/`), imported image
537
+ That directory holds per-session SQLite databases and event logs
538
+ (`sessions/<id>/`), background Bash task logs (`bash/`), imported image
562
539
  assets (`assets/images/`), and prompt history (`prompt-history.jsonl`).
563
540
  Workspaces stay clean: no `.tinker/` directory is created inside them.
564
541
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tinker-agent",
3
- "version": "2.10.0",
3
+ "version": "2.12.0",
4
4
  "description": "A personal coding agent with an interactive TUI and one-shot CLI.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -66,14 +66,15 @@
66
66
  "docs:check": "bun scripts/render-public-contract-docs.ts --check",
67
67
  "tinker": "bun src/cli/index.ts",
68
68
  "check": "bun run check:source-lines && bun run typecheck && bun run format:check && bun run lint && bun run docs:check && bun run test && bun run bench:smoke",
69
- "check:fast": "bun run check:source-lines && bun run typecheck && bun run format:check && bun run lint && bun run test:fast",
69
+ "check:fast": "bun run check:source-lines && bun run typecheck && bun run format:check && bun run lint:fast && bun run test:fast",
70
70
  "check:source-lines": "bun scripts/check-source-lines.ts",
71
71
  "format": "biome format --write .",
72
72
  "format:check": "biome format .",
73
73
  "lint": "eslint \"bin/**/*.js\" \"src/**/*.{ts,tsx}\" \"scripts/**/*.ts\" \"packages/**/*.ts\" --max-warnings=0",
74
+ "lint:fast": "bun run lint --cache --cache-strategy content --cache-location .cache/eslint-fast/",
74
75
  "lint:fix": "eslint \"bin/**/*.js\" \"src/**/*.{ts,tsx}\" \"scripts/**/*.ts\" \"packages/**/*.ts\" --fix",
75
76
  "test": "FORCE_COLOR=0 bun test",
76
- "test:fast": "FORCE_COLOR=0 bun test --path-ignore-patterns='**/*pty*.test.ts' --path-ignore-patterns='**/native-host-integration.test.ts'",
77
+ "test:fast": "bun scripts/run-test-shards.ts --path-ignore-patterns='**/*pty*.test.ts' --path-ignore-patterns='**/native-host-integration.test.ts'",
77
78
  "test:e2e": "FORCE_COLOR=0 bun test src/__tests__/cli-pty src/__tests__/pty-tui-harness packages/tinker-chrome/__tests__/native-host-integration",
78
79
  "typecheck": "tsc --noEmit"
79
80
  },
package/src/agent/loop.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import type { ModelRequestFailedData } from "../events/types";
2
+ import type { ProviderRetryDecision } from "./runtime-provider-retry";
1
3
  import {
2
4
  materializeModelRequest,
3
5
  type MaterializedModelRequest,
@@ -74,6 +76,11 @@ export type RunAgentInput = {
74
76
  usage: ContextUsageSnapshot;
75
77
  };
76
78
  transientRetryDelaysMs?: readonly number[];
79
+ requestProviderRetry?: (
80
+ iteration: IterationIdentity,
81
+ failure: ModelRequestFailedData,
82
+ signal: AbortSignal,
83
+ ) => Promise<ProviderRetryDecision>;
77
84
  };
78
85
 
79
86
  export class FatalAgentTurnError extends Error {
@@ -196,12 +203,13 @@ export async function runAgent(input: RunAgentInput): Promise<RunAgentResult> {
196
203
  return failedResult(error, iteration);
197
204
  }
198
205
 
206
+ let maxAttempts = MODEL_REQUEST_MAX_ATTEMPTS;
199
207
  await input.runtimeSession.append({
200
208
  type: "model.request.started",
201
209
  ...iteration,
202
210
  data: {
203
211
  attemptNumber: 1,
204
- maxAttempts: MODEL_REQUEST_MAX_ATTEMPTS,
212
+ maxAttempts,
205
213
  },
206
214
  });
207
215
 
@@ -240,7 +248,7 @@ export async function runAgent(input: RunAgentInput): Promise<RunAgentResult> {
240
248
  ...iteration,
241
249
  data: {
242
250
  attemptNumber,
243
- maxAttempts: MODEL_REQUEST_MAX_ATTEMPTS,
251
+ maxAttempts,
244
252
  },
245
253
  });
246
254
  }
@@ -289,19 +297,47 @@ export async function runAgent(input: RunAgentInput): Promise<RunAgentResult> {
289
297
  transientRetries,
290
298
  transientRetryDelaysMs,
291
299
  });
300
+ const failure = modelRequestFailureData({
301
+ error,
302
+ request,
303
+ attemptNumber,
304
+ maxAttempts,
305
+ retryDisposition: decision.disposition,
306
+ ...(decision.disposition === "scheduled" && decision.delayMs > 0
307
+ ? { retryDelayMs: decision.delayMs }
308
+ : {}),
309
+ });
292
310
  await input.runtimeSession.append({
293
311
  type: "model.request.failed",
294
312
  ...iteration,
295
- data: modelRequestFailureData({
296
- error,
297
- request,
298
- attemptNumber,
299
- retryDisposition: decision.disposition,
300
- ...(decision.disposition === "scheduled" && decision.delayMs > 0
301
- ? { retryDelayMs: decision.delayMs }
302
- : {}),
303
- }),
313
+ data: failure,
304
314
  });
315
+ if (decision.disposition === "exhausted" && input.requestProviderRetry) {
316
+ let retry: ProviderRetryDecision;
317
+ try {
318
+ retry = await input.requestProviderRetry(iteration, failure, input.signal);
319
+ } catch (retryError) {
320
+ if (input.signal.aborted) {
321
+ return cancelledResult(
322
+ cancellation(input.signal, iteration, "model_request"),
323
+ iteration,
324
+ );
325
+ }
326
+ throw retryError;
327
+ }
328
+ if (input.signal.aborted) {
329
+ return cancelledResult(
330
+ cancellation(input.signal, iteration, "model_request"),
331
+ iteration,
332
+ );
333
+ }
334
+ if (retry === "retry") {
335
+ reasoningOnlyRetries = 0;
336
+ transientRetries = 0;
337
+ maxAttempts = attemptNumber + MODEL_REQUEST_MAX_ATTEMPTS;
338
+ continue;
339
+ }
340
+ }
305
341
 
306
342
  if (decision.disposition === "scheduled") {
307
343
  if (decision.kind === "reasoning_only") {
@@ -349,7 +385,7 @@ export async function runAgent(input: RunAgentInput): Promise<RunAgentResult> {
349
385
  ...iteration,
350
386
  data: {
351
387
  attemptNumber: successfulAttempt,
352
- maxAttempts: MODEL_REQUEST_MAX_ATTEMPTS,
388
+ maxAttempts,
353
389
  output: modelOutput,
354
390
  },
355
391
  });
@@ -823,6 +859,7 @@ function modelRequestFailureData(input: {
823
859
  error: unknown;
824
860
  request: PreparedModelRequest;
825
861
  attemptNumber: number;
862
+ maxAttempts: number;
826
863
  retryDisposition: "scheduled" | "not_retryable" | "exhausted";
827
864
  retryDelayMs?: number;
828
865
  }) {
@@ -830,7 +867,7 @@ function modelRequestFailureData(input: {
830
867
  input.error instanceof ProviderResponseError ? input.error : undefined;
831
868
  return {
832
869
  attemptNumber: input.attemptNumber,
833
- maxAttempts: MODEL_REQUEST_MAX_ATTEMPTS,
870
+ maxAttempts: input.maxAttempts,
834
871
  code: providerError?.code ?? ("provider_request_error" as const),
835
872
  retryDisposition: input.retryDisposition,
836
873
  ...(input.retryDelayMs === undefined ? {} : { retryDelayMs: input.retryDelayMs }),
@@ -0,0 +1,115 @@
1
+ import type { AgentEventInput, ModelRequestFailedData } from "../events/types";
2
+ import type { IterationIdentity } from "./types";
3
+ import { cancellationError } from "./turn-cancellation";
4
+
5
+ export type ProviderRetryDecision = "retry" | "stop";
6
+ export type ProviderRetryRequest = {
7
+ readonly requestId: string;
8
+ readonly failure: ModelRequestFailedData;
9
+ };
10
+ export type ProviderRetrySnapshot = { readonly pending?: ProviderRetryRequest };
11
+ export const EMPTY_PROVIDER_RETRY: ProviderRetrySnapshot = Object.freeze({});
12
+
13
+ type PendingRetry = {
14
+ request: ProviderRetryRequest;
15
+ iteration: IterationIdentity;
16
+ startedAt: number;
17
+ signal: AbortSignal;
18
+ resolve: (decision: ProviderRetryDecision) => void;
19
+ reject: (error: unknown) => void;
20
+ removeAbortListener: () => void;
21
+ };
22
+
23
+ /** Process-local interaction; the original turn and model request remain open. */
24
+ export class RuntimeProviderRetry {
25
+ private snapshot: ProviderRetrySnapshot = EMPTY_PROVIDER_RETRY;
26
+ private pending?: PendingRetry;
27
+ private readonly listeners = new Set<() => void>();
28
+
29
+ constructor(private readonly append: (event: AgentEventInput) => Promise<void>) {}
30
+
31
+ read(): ProviderRetrySnapshot {
32
+ return this.snapshot;
33
+ }
34
+
35
+ subscribe(listener: () => void): () => void {
36
+ this.listeners.add(listener);
37
+ return () => this.listeners.delete(listener);
38
+ }
39
+
40
+ async request(
41
+ iteration: IterationIdentity,
42
+ failure: ModelRequestFailedData,
43
+ signal: AbortSignal,
44
+ ): Promise<ProviderRetryDecision> {
45
+ if (this.pending !== undefined) throw new Error("Provider retry already pending.");
46
+ if (signal.aborted) throw cancellationError(signal);
47
+ await this.append({ type: "model.retry.requested", ...iteration, data: failure });
48
+ return new Promise((resolve, reject) => {
49
+ const request = Object.freeze({
50
+ requestId: `${iteration.iterationId}:${failure.attemptNumber}`,
51
+ failure: Object.freeze({ ...failure }),
52
+ });
53
+ const pending: PendingRetry = {
54
+ request,
55
+ iteration,
56
+ startedAt: Date.now(),
57
+ signal,
58
+ resolve,
59
+ reject,
60
+ removeAbortListener: () => signal.removeEventListener("abort", onAbort),
61
+ };
62
+ const onAbort = () => {
63
+ // settle rejects the waiting loop too; do not leave a detached rejection.
64
+ void this.settle(pending, "cancelled").catch(() => undefined);
65
+ };
66
+ this.pending = pending;
67
+ this.snapshot = Object.freeze({ pending: request });
68
+ signal.addEventListener("abort", onAbort, { once: true });
69
+ if (signal.aborted) onAbort();
70
+ else this.notify();
71
+ });
72
+ }
73
+
74
+ async resolve(requestId: string, decision: ProviderRetryDecision): Promise<void> {
75
+ const pending = this.pending;
76
+ if (pending === undefined || pending.request.requestId !== requestId) {
77
+ throw new Error("Provider retry question is no longer pending.");
78
+ }
79
+ if (decision !== "retry" && decision !== "stop") {
80
+ throw new Error("Invalid provider retry decision.");
81
+ }
82
+ await this.settle(pending, decision);
83
+ }
84
+
85
+ private async settle(
86
+ pending: PendingRetry,
87
+ decision: ProviderRetryDecision | "cancelled",
88
+ ): Promise<void> {
89
+ if (this.pending !== pending) return;
90
+ this.pending = undefined;
91
+ this.snapshot = EMPTY_PROVIDER_RETRY;
92
+ pending.removeAbortListener();
93
+ this.notify();
94
+ try {
95
+ await this.append({
96
+ type: "model.retry.resolved",
97
+ ...pending.iteration,
98
+ data: {
99
+ attemptNumber: pending.request.failure.attemptNumber,
100
+ decision,
101
+ durationMs: Date.now() - pending.startedAt,
102
+ },
103
+ });
104
+ if (decision === "cancelled") pending.reject(cancellationError(pending.signal));
105
+ else pending.resolve(decision);
106
+ } catch (error) {
107
+ pending.reject(error);
108
+ throw error;
109
+ }
110
+ }
111
+
112
+ private notify(): void {
113
+ for (const listener of this.listeners) listener();
114
+ }
115
+ }
@@ -1,3 +1,7 @@
1
+ import type {
2
+ ProviderRetryDecision,
3
+ ProviderRetrySnapshot,
4
+ } from "./runtime-provider-retry";
1
5
  import type { PublicToolingConfig } from "../cli/public-config-contract";
2
6
  import type { ContextAutomationPolicy } from "../context/context-automation-policy";
3
7
  import type {
@@ -11,7 +15,7 @@ import type { ContextSurfaceComponent } from "../context/context-surface";
11
15
  import type { ToolCompletionInput } from "../context/protocol-frame";
12
16
  import type { EventSink } from "../events/event-sink";
13
17
  import type { AgentEventInput, AgentEventType } from "../events/types";
14
- import type { RuntimeIdFactory, SessionId, TurnId } from "../ids/runtime-id";
18
+ import type { RuntimeIdFactory, SessionId } from "../ids/runtime-id";
15
19
  import type { ImportedImageAsset } from "../image/image-asset-store";
16
20
  import type { ImageAssetRef, UserMessage } from "../image/image-types";
17
21
  import type { ProjectInstructionManifest } from "../instructions/project-instructions";
@@ -24,11 +28,7 @@ import type {
24
28
  } from "../model/model-context-profile";
25
29
  import type { ReasoningEffortSnapshot } from "../model/reasoning-effort";
26
30
  import type { ObservationBuilder } from "../observation/observation-builder";
27
- import type {
28
- CompletedTurnSnapshot,
29
- SessionRecoveryResult,
30
- SessionStore,
31
- } from "../session/session-store";
31
+ import type { SessionRecoveryResult, SessionStore } from "../session/session-store";
32
32
  import type { SkillCatalogSnapshot } from "../skills/skill-loader";
33
33
  import type { createDefaultTooling } from "../tools/registry";
34
34
  import type { TurnUndoResult } from "../tools/turn-undo-manager";
@@ -118,6 +118,12 @@ export type RuntimeSession = {
118
118
  subscribeBashGuard(listener: () => void): () => void;
119
119
  setYoloMode(enabled: boolean): void;
120
120
  resolveBashConfirmation(decision: "allow" | "deny"): Promise<void>;
121
+ providerRetry(): ProviderRetrySnapshot;
122
+ subscribeProviderRetry(listener: () => void): () => void;
123
+ resolveProviderRetry(
124
+ requestId: string,
125
+ decision: ProviderRetryDecision,
126
+ ): Promise<void>;
121
127
  askUser(): AskUserSnapshot;
122
128
  subscribeAskUser(listener: () => void): () => void;
123
129
  resolveAskUser(response: AskUserResolution): Promise<void>;
@@ -201,25 +207,6 @@ export type SkillsUpdateSummary = {
201
207
  readonly addedOverrideCount: number;
202
208
  };
203
209
 
204
- export type CompletedTurnHookInput = {
205
- readonly workspaceRoot: string;
206
- readonly sessionId: SessionId;
207
- readonly turnId: TurnId;
208
- readonly snapshot: CompletedTurnSnapshot;
209
- };
210
-
211
- export type CompletedTurnHookFailure = {
212
- readonly workspaceRoot: string;
213
- readonly sessionId: SessionId;
214
- readonly turnId: TurnId;
215
- readonly reason: "completed_turn_snapshot_failed" | "completed_turn_enqueue_failed";
216
- };
217
-
218
- export type CompletedTurnHook = {
219
- enqueue(input: CompletedTurnHookInput): void;
220
- recordFailure(input: CompletedTurnHookFailure): void;
221
- };
222
-
223
210
  export type CommonRuntimeSessionInput = {
224
211
  workspaceRoot: string;
225
212
  homeRoot?: string;
@@ -244,13 +231,10 @@ export type CommonRuntimeSessionInput = {
244
231
  webFetchRefiner?: Refiner;
245
232
  toolingConfig?: PublicToolingConfig;
246
233
  memorySearch?: ToolExecutor;
247
- memoryGet?: ToolExecutor;
248
234
  memoryCreate?: ToolExecutor;
249
- memoryUpdate?: ToolExecutor;
250
- memoryDelete?: ToolExecutor;
251
- completedTurnHook?: CompletedTurnHook;
252
235
  enableTurnUndo?: boolean;
253
236
  enableAskUser?: boolean;
237
+ enableProviderRetryPrompt?: boolean;
254
238
  bashGuard?: {
255
239
  readonly mode: "guard" | "yolo";
256
240
  readonly source: Exclude<BashGuardSource, "session">;