tinker-agent 2.5.0 → 2.7.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 (36) hide show
  1. package/CHANGELOG.md +42 -1
  2. package/README.md +30 -7
  3. package/package.json +1 -1
  4. package/src/agent/loop.ts +22 -0
  5. package/src/agent/runtime-session.ts +158 -0
  6. package/src/cli/main.ts +2 -0
  7. package/src/cli/public-config-contract.ts +1 -1
  8. package/src/cli/tui-runner.tsx +16 -1
  9. package/src/events/stdout-event-printer.ts +14 -0
  10. package/src/events/types.ts +9 -0
  11. package/src/memory/contracts.ts +46 -0
  12. package/src/memory/memory-coordinator.ts +445 -4
  13. package/src/memory/memory-create-tool.ts +117 -0
  14. package/src/memory/memory-delete-tool.ts +88 -0
  15. package/src/memory/memory-store.ts +239 -0
  16. package/src/memory/memory-update-tool.ts +142 -0
  17. package/src/observation/observation-builder.ts +70 -0
  18. package/src/session/session-clone-helpers.ts +249 -0
  19. package/src/session/session-compatibility-codec.ts +401 -0
  20. package/src/session/session-store-contracts.ts +304 -0
  21. package/src/session/session-store-filesystem.ts +245 -0
  22. package/src/session/session-store-record-codecs.ts +1064 -0
  23. package/src/session/session-store-value-codecs.ts +156 -0
  24. package/src/session/session-store.ts +234 -3023
  25. package/src/session/session-tool-result-codec.ts +594 -0
  26. package/src/tools/ask-user.ts +100 -0
  27. package/src/tools/grep.ts +1 -3
  28. package/src/tools/registry.ts +30 -0
  29. package/src/tools/types.ts +71 -0
  30. package/src/tui/app.tsx +35 -5
  31. package/src/tui/components/ask-user.tsx +61 -0
  32. package/src/tui/components/footer.tsx +14 -1
  33. package/src/tui/components/prompt-input.tsx +4 -0
  34. package/src/tui/components/resume-session-picker.tsx +118 -37
  35. package/src/tui/event-store.ts +57 -0
  36. package/src/tui/tui-session-controller.ts +8 -0
package/CHANGELOG.md CHANGED
@@ -5,6 +5,45 @@ All notable user-facing changes to Tinker are documented here. The project follo
5
5
 
6
6
  ## [Unreleased]
7
7
 
8
+ ## [2.7.0] - 2026-09-04
9
+
10
+ ### Added
11
+
12
+ - Add `AskUser`, an interactive tool that lets the agent pause on a material
13
+ ambiguity and present two to six complete choices in the TUI before resuming
14
+ the same turn with the selected answer.
15
+ - Add model-accessible `MemoryCreate`, `MemoryUpdate`, and `MemoryDelete` tools
16
+ for explicitly maintaining global memories shared across sessions and
17
+ workspaces.
18
+
19
+ ### Changed
20
+
21
+ - Redesign the session resume picker as a compact table, making session metadata
22
+ easier to scan while preserving keyboard navigation and search.
23
+
24
+ ### Fixed
25
+
26
+ - Give the long-history PTY resume fixture enough time on slower Linux CI
27
+ runners, avoiding a false timeout while the model is still producing the
28
+ expected response.
29
+
30
+ ## [2.6.0] - 2026-09-03
31
+
32
+ ### Added
33
+
34
+ - Show the installed Tinker version at the end of the interactive prompt status
35
+ bar.
36
+
37
+ ### Changed
38
+
39
+ - Raise the default maximum agent-loop iterations per turn from 512 to 65,536,
40
+ allowing longer autonomous tasks without requiring configuration changes.
41
+
42
+ ### Fixed
43
+
44
+ - Preserve brace expressions in Grep glob filters, so patterns such as
45
+ `**/*.{ts,tsx}` are passed to ripgrep intact.
46
+
8
47
  ## [2.5.0] - 2026-09-03
9
48
 
10
49
  ### Added
@@ -307,7 +346,9 @@ All notable user-facing changes to Tinker are documented here. The project follo
307
346
  - First formal npm release under the `tinker-agent` package name with the `tinker`
308
347
  executable.
309
348
 
310
- [Unreleased]: https://github.com/ishowshao/tinker/compare/v2.5.0...HEAD
349
+ [Unreleased]: https://github.com/ishowshao/tinker/compare/v2.7.0...HEAD
350
+ [2.7.0]: https://github.com/ishowshao/tinker/releases/tag/v2.7.0
351
+ [2.6.0]: https://github.com/ishowshao/tinker/releases/tag/v2.6.0
311
352
  [2.5.0]: https://github.com/ishowshao/tinker/releases/tag/v2.5.0
312
353
  [2.4.0]: https://github.com/ishowshao/tinker/releases/tag/v2.4.0
313
354
  [2.3.0]: https://github.com/ishowshao/tinker/releases/tag/v2.3.0
package/README.md CHANGED
@@ -1,8 +1,10 @@
1
1
  # Tinker
2
2
 
3
- **Tinker** is a personal coding agent — an interactive TUI (Terminal User Interface) and one-shot CLI tool that drives an LLM in an agent loop with file, search, shell, and MCP tools to read and modify a local workspace.
3
+ **Tinker** is a personal coding-agent harness — an interactive TUI (Terminal User Interface) and one-shot CLI that drives an LLM in an agent loop with file, search, shell, and MCP tools to read and modify a local workspace.
4
4
 
5
- Built with [Bun](https://bun.sh) + TypeScript ESM, powered by [Ink](https://github.com/vadimdemedes/ink) (React for CLIs).
5
+ **Tinker is designed for models that work over extremely long horizons — potentially as persistent agents that continue indefinitely, rather than as disposable chat sessions.** Its architecture treats the model's context window as a bounded working set, not as the source of truth. Immutable canonical history, durable sessions, protocol-safe recovery, deterministic context revisions, Recall-addressable cold state, and context-pressure management allow work to continue across compaction, process restarts, and context-window limits.
6
+
7
+ This does not pretend that any model has infinite tokens or guarantee that it will recall every relevant fact. It means the harness is designed so that history remains durable and recoverable while the model repeatedly operates on a bounded, valid view of an ongoing session.
6
8
 
7
9
  ## Features
8
10
 
@@ -27,16 +29,36 @@ Built with [Bun](https://bun.sh) + TypeScript ESM, powered by [Ink](https://gith
27
29
  - **Turn cancellation**: Users can cancel an ongoing turn safely, with protocol-safe synthetic tool messages.
28
30
  - **Context metering**: Budget-aware context management with protocol validation before sending requests to the model.
29
31
  - **Deterministic context compaction**: Idle sessions can swap eligible historical tool output into Recall-addressable placeholders without calling the model.
30
- - **Infinite Context architecture**: Immutable canonical history, deterministic
31
- context revisions, Recall-addressable cold state, and qualified prefix retirement
32
- keep long-running sessions recoverable without pretending the model has infinite
33
- tokens. See the [technical design](docs/infinite-context-technical-design.md).
32
+ - **Infinite Context architecture**: Immutable canonical history, deterministic context revisions, Recall-addressable cold state, and qualified prefix retirement support sessions designed to continue indefinitely without pretending the model has infinite tokens. See the [technical design](docs/infinite-context-technical-design.md).
34
33
  - **Choice of models**: Uses an OpenAI-compatible Chat Completions transport with
35
34
  explicit model and context limits. Actual provider support must be established
36
35
  by a qualification matrix; transport compatibility alone is not a guarantee.
37
36
 
38
37
  ## Quick Start
39
38
 
39
+ ### Install with your existing agent
40
+
41
+ If you already use a coding agent, you can ask it to read Tinker's documentation, install the package, and prepare a complete local configuration for you. Values it cannot safely determine can be left as clearly named placeholders; when setup is complete, the agent should tell you exactly which file to edit and what each placeholder expects.
42
+
43
+ Copy and send this prompt to your existing agent:
44
+
45
+ ```text
46
+ Open https://github.com/ishowshao/tinker and read Tinker's README and linked model-configuration documentation, then install and configure Tinker on this machine.
47
+
48
+ Create a complete model profile configuration in an appropriate local file. Use clearly named placeholders for any required values that cannot be determined from the documentation or the current environment; do not invent API credentials, endpoint URLs, model names, or model limits. Configure Tinker to use that file, and verify the installation and configuration as far as possible without making a live model request.
49
+
50
+ When finished, tell me:
51
+ 1. the exact path of the configuration file you created;
52
+ 2. every placeholder I still need to replace;
53
+ 3. what value each placeholder expects;
54
+ 4. any command I need to run after filling them in;
55
+ 5. the command to start Tinker.
56
+
57
+ Do not ask me for the missing values during setup unless proceeding would be unsafe. Prefer creating a structurally complete configuration with placeholders so I can fill them in locally afterward.
58
+ ```
59
+
60
+ ### Install manually
61
+
40
62
  ```bash
41
63
  npm install --global tinker-agent
42
64
 
@@ -149,7 +171,7 @@ are required. Boolean environment values accept case-insensitive `true/false`,
149
171
  | `TINKER_STREAM` | Model | Env mode | No | Boolean | `true` | No | Use streaming transport for the selected model API. |
150
172
  | `TINKER_WEBFETCH_REFINE_MODEL` | Model | Env mode | No | Non-empty string | — | No | Optional WebFetch refiner model; currently must match TINKER_MODEL. |
151
173
  | `TINKER_WORKSPACE` | Workspace | All modes | No | Non-empty string | Process cwd | No | Workspace path. A leading ~ expands to the home directory; other relative paths resolve from the process cwd. |
152
- | `TINKER_MAX_ITERATIONS` | Workspace | All modes | No | Positive integer | `512` | No | Maximum agent-loop iterations per turn. |
174
+ | `TINKER_MAX_ITERATIONS` | Workspace | All modes | No | Positive integer | `65536` | No | Maximum agent-loop iterations per turn. |
153
175
  | `EXA_API_KEY` | Tooling | All modes | No | Non-empty string | — | Yes | Enables WebSearch and the Exa WebFetch backend when set. |
154
176
  | `TINKER_MCP_TIMEOUT_MS` | Tooling | All modes | No | Positive integer | `60000` | No | MCP tool-call timeout in milliseconds. |
155
177
  | `TINKER_MCP_MAX_OBSERVATION_CHARS` | Tooling | All modes | No | Positive integer | `40000` | No | Maximum model-visible characters in one MCP result. |
@@ -513,6 +535,7 @@ Runtime data lives in ~/.tinker/ (sessions, bash tasks, assets), not in the repo
513
535
  - **Fast-fail**: Validate assumptions early and return clear errors close to the source. Structured failures allow the model to correct and retry.
514
536
  - **Model sees only text**: Tool execution results are rendered into readable text for the model. Raw result data with extra detail is kept for event logs and the TUI.
515
537
  - **Protocol safety**: All tool calls produce protocol-safe messages — even cancellations, fatal errors, or interruptions generate well-formed tool messages so the agent loop can continue.
538
+ - **Long-horizon continuity**: Treat the context window as a replaceable working set over durable canonical history. Sessions should remain resumable and historically recoverable across compaction, interruption, and process restarts.
516
539
  - **Session durability**: Every turn, iteration, and tool call is committed to the SQLite ledger before the model is called, enabling reliable resume and history recall.
517
540
 
518
541
  ## Requirements
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tinker-agent",
3
- "version": "2.5.0",
3
+ "version": "2.7.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",
package/src/agent/loop.ts CHANGED
@@ -384,6 +384,28 @@ export async function runAgent(input: RunAgentInput): Promise<RunAgentResult> {
384
384
  });
385
385
  }
386
386
 
387
+ if (toolCalls.some((call) => call.name === "AskUser") && toolCalls.length !== 1) {
388
+ const detail =
389
+ "AskUser must be the only tool call in an assistant response. Call it alone on the next iteration.";
390
+ const completions = toolCalls.map((call, index) => {
391
+ requireCallInIteration(call, iteration, index + 1);
392
+ return {
393
+ call,
394
+ kind: "synthetic" as const,
395
+ reason: "failed_active" as const,
396
+ detail,
397
+ };
398
+ });
399
+ input.ledger.commitToolCompletions(completions);
400
+ await input.runtimeSession.append({
401
+ type: "agent.iteration.finished",
402
+ ...iteration,
403
+ data: { outcome: "continue", toolCallCount: toolCalls.length },
404
+ });
405
+ input.runtimeSession.finishIterationForContinuation(iteration);
406
+ continue;
407
+ }
408
+
387
409
  for (let callIndex = 0; callIndex < toolCalls.length; callIndex += 1) {
388
410
  const call = requireToolCall(toolCalls, callIndex);
389
411
  requireCallInIteration(call, iteration, callIndex + 1);
@@ -80,6 +80,8 @@ import {
80
80
  type ContextStatusRawResult,
81
81
  type ContextSwapCandidatesRawResult,
82
82
  type ContextSwapRawResult,
83
+ type AskUserRequest,
84
+ type AskUserResponse,
83
85
  type ToolExecutor,
84
86
  } from "../tools/types";
85
87
  import type { TurnUndoResult } from "../tools/turn-undo-manager";
@@ -202,9 +204,20 @@ export type RuntimeSession = {
202
204
  subscribeBashGuard(listener: () => void): () => void;
203
205
  setYoloMode(enabled: boolean): void;
204
206
  resolveBashConfirmation(decision: "allow" | "deny"): Promise<void>;
207
+ askUser(): AskUserSnapshot;
208
+ subscribeAskUser(listener: () => void): () => void;
209
+ resolveAskUser(response: AskUserResolution): Promise<void>;
205
210
  dispose(reason: SessionDisposeReason): Promise<void>;
206
211
  };
207
212
 
213
+ export type AskUserSnapshot = {
214
+ readonly pending?: AskUserRequest;
215
+ };
216
+
217
+ export type AskUserResolution =
218
+ | { readonly outcome: "selected"; readonly selectedIndex: number }
219
+ | { readonly outcome: "dismissed" };
220
+
208
221
  export type BashGuardSource = "default" | "environment" | "cli" | "session";
209
222
 
210
223
  export type BashGuardSnapshot = {
@@ -322,8 +335,12 @@ type CommonRuntimeSessionInput = {
322
335
  toolingConfig?: PublicToolingConfig;
323
336
  memorySearch?: ToolExecutor;
324
337
  memoryGet?: ToolExecutor;
338
+ memoryCreate?: ToolExecutor;
339
+ memoryUpdate?: ToolExecutor;
340
+ memoryDelete?: ToolExecutor;
325
341
  completedTurnHook?: CompletedTurnHook;
326
342
  enableTurnUndo?: boolean;
343
+ enableAskUser?: boolean;
327
344
  bashGuard?: {
328
345
  readonly mode: "guard" | "yolo";
329
346
  readonly source: Exclude<BashGuardSource, "session">;
@@ -490,6 +507,16 @@ class DefaultRuntimeSession implements RuntimeSession {
490
507
  private bashGuardSource: BashGuardSource;
491
508
  private bashGuardSnapshot: BashGuardSnapshot;
492
509
  private readonly bashGuardListeners = new Set<() => void>();
510
+ private askUserSnapshot: AskUserSnapshot = Object.freeze({});
511
+ private readonly askUserListeners = new Set<() => void>();
512
+ private pendingAskUser?: {
513
+ readonly request: AskUserRequest;
514
+ readonly startedAt: number;
515
+ readonly call: ToolCallIdentity;
516
+ readonly resolve: (response: AskUserResponse) => void;
517
+ readonly reject: (error: unknown) => void;
518
+ readonly removeAbortListener: () => void;
519
+ };
493
520
  private assistantTextDeltaSinkDisabled = false;
494
521
  private pendingBashConfirmation?: {
495
522
  readonly command: string;
@@ -695,6 +722,15 @@ class DefaultRuntimeSession implements RuntimeSession {
695
722
  ...(input.enableTurnUndo === true ? { enableTurnUndo: true } : {}),
696
723
  webFetchRefiner: input.webFetchRefiner,
697
724
  toolingConfig: input.toolingConfig,
725
+ ...(input.enableAskUser === true
726
+ ? {
727
+ askUser: (
728
+ call: ToolCallIdentity,
729
+ request: AskUserRequest,
730
+ signal: AbortSignal,
731
+ ) => session.requestUserAnswer(call, request, signal),
732
+ }
733
+ : {}),
698
734
  bashGuard: {
699
735
  surface: input.bashGuard?.surface ?? "one-shot",
700
736
  confirm: (call, request, signal) =>
@@ -704,6 +740,15 @@ class DefaultRuntimeSession implements RuntimeSession {
704
740
  ? {}
705
741
  : { memorySearch: input.memorySearch }),
706
742
  ...(input.memoryGet === undefined ? {} : { memoryGet: input.memoryGet }),
743
+ ...(input.memoryCreate === undefined
744
+ ? {}
745
+ : { memoryCreate: input.memoryCreate }),
746
+ ...(input.memoryUpdate === undefined
747
+ ? {}
748
+ : { memoryUpdate: input.memoryUpdate }),
749
+ ...(input.memoryDelete === undefined
750
+ ? {}
751
+ : { memoryDelete: input.memoryDelete }),
707
752
  ...(session.skillCatalog.skills.size === 0
708
753
  ? {}
709
754
  : {
@@ -1204,6 +1249,119 @@ class DefaultRuntimeSession implements RuntimeSession {
1204
1249
  }
1205
1250
  }
1206
1251
 
1252
+ askUser(): AskUserSnapshot {
1253
+ return this.askUserSnapshot;
1254
+ }
1255
+
1256
+ subscribeAskUser(listener: () => void): () => void {
1257
+ this.askUserListeners.add(listener);
1258
+ return () => this.askUserListeners.delete(listener);
1259
+ }
1260
+
1261
+ async resolveAskUser(response: AskUserResolution): Promise<void> {
1262
+ const pending = this.pendingAskUser;
1263
+ if (pending === undefined) {
1264
+ throw new Error("No AskUser question is pending.");
1265
+ }
1266
+ let result: AskUserResponse;
1267
+ if (response.outcome === "selected") {
1268
+ if (!Number.isSafeInteger(response.selectedIndex)) {
1269
+ throw new Error("AskUser selectedIndex must be an integer.");
1270
+ }
1271
+ const option = pending.request.options[response.selectedIndex];
1272
+ if (option === undefined) {
1273
+ throw new Error("AskUser selectedIndex is out of range.");
1274
+ }
1275
+ result = { outcome: "selected", answer: option.description };
1276
+ } else {
1277
+ result = { outcome: "dismissed" };
1278
+ }
1279
+ this.pendingAskUser = undefined;
1280
+ this.askUserSnapshot = Object.freeze({});
1281
+ pending.removeAbortListener();
1282
+ await this.append({
1283
+ type: "tool.user_question.resolved",
1284
+ ...pending.call,
1285
+ data: {
1286
+ ...result,
1287
+ durationMs: Date.now() - pending.startedAt,
1288
+ },
1289
+ });
1290
+ pending.resolve(result);
1291
+ this.notifyAskUserListeners();
1292
+ }
1293
+
1294
+ private async requestUserAnswer(
1295
+ call: ToolCallIdentity,
1296
+ request: AskUserRequest,
1297
+ signal: AbortSignal,
1298
+ ): Promise<AskUserResponse> {
1299
+ if (this.pendingAskUser !== undefined) {
1300
+ throw new Error("Another AskUser question is already pending.");
1301
+ }
1302
+ if (this.pendingBashConfirmation !== undefined) {
1303
+ throw new Error("Cannot ask the user while a Bash confirmation is pending.");
1304
+ }
1305
+ if (signal.aborted) {
1306
+ throw cancellationError(signal);
1307
+ }
1308
+ const startedAt = Date.now();
1309
+ await this.append({
1310
+ type: "tool.user_question.requested",
1311
+ ...call,
1312
+ data: request,
1313
+ });
1314
+ return new Promise<AskUserResponse>((resolve, reject) => {
1315
+ const onAbort = () => {
1316
+ const pending = this.pendingAskUser;
1317
+ if (pending?.call.toolCallId !== call.toolCallId) {
1318
+ return;
1319
+ }
1320
+ this.pendingAskUser = undefined;
1321
+ this.askUserSnapshot = Object.freeze({});
1322
+ void this.append({
1323
+ type: "tool.user_question.resolved",
1324
+ ...call,
1325
+ data: {
1326
+ outcome: "cancelled",
1327
+ durationMs: Date.now() - startedAt,
1328
+ },
1329
+ }).finally(() => {
1330
+ reject(cancellationError(signal));
1331
+ this.notifyAskUserListeners();
1332
+ });
1333
+ };
1334
+ signal.addEventListener("abort", onAbort, { once: true });
1335
+ const immutableRequest = Object.freeze({
1336
+ question: request.question,
1337
+ options: Object.freeze(
1338
+ request.options.map((option) =>
1339
+ Object.freeze({ description: option.description }),
1340
+ ),
1341
+ ),
1342
+ });
1343
+ this.pendingAskUser = {
1344
+ request: immutableRequest,
1345
+ startedAt,
1346
+ call,
1347
+ resolve,
1348
+ reject,
1349
+ removeAbortListener: () => signal.removeEventListener("abort", onAbort),
1350
+ };
1351
+ this.askUserSnapshot = Object.freeze({ pending: immutableRequest });
1352
+ this.notifyAskUserListeners();
1353
+ if (signal.aborted) {
1354
+ onAbort();
1355
+ }
1356
+ });
1357
+ }
1358
+
1359
+ private notifyAskUserListeners(): void {
1360
+ for (const listener of this.askUserListeners) {
1361
+ listener();
1362
+ }
1363
+ }
1364
+
1207
1365
  async importImage(
1208
1366
  sourcePath: string,
1209
1367
  signal: AbortSignal,
package/src/cli/main.ts CHANGED
@@ -51,6 +51,7 @@ type TuiRunner = {
51
51
  readonly publicConfig: ResolvedPublicConfig;
52
52
  readonly initialRunnerConfig: RunnerConfig;
53
53
  readonly env: NodeJS.ProcessEnv;
54
+ readonly version: string;
54
55
  }) => Promise<void>;
55
56
  };
56
57
 
@@ -187,6 +188,7 @@ export async function main(
187
188
  publicConfig,
188
189
  initialRunnerConfig: runnerConfig,
189
190
  env,
191
+ version: metadata.version,
190
192
  });
191
193
  return finish(0);
192
194
  } catch (error) {
@@ -135,7 +135,7 @@ export const PUBLIC_CONFIG_FIELDS = Object.freeze([
135
135
  valueKind: "positive-integer",
136
136
  requiredIn: "never",
137
137
  appliesIn: "always",
138
- defaultValue: 512,
138
+ defaultValue: 65_536,
139
139
  secret: false,
140
140
  section: "workspace",
141
141
  description: "Maximum agent-loop iterations per turn.",
@@ -54,6 +54,7 @@ export type RunTuiOptions = {
54
54
  readonly publicConfig: ResolvedPublicConfig;
55
55
  readonly initialRunnerConfig: RunnerConfig;
56
56
  readonly env: NodeJS.ProcessEnv;
57
+ readonly version: string;
57
58
  };
58
59
 
59
60
  export async function runTui(options: RunTuiOptions): Promise<void> {
@@ -117,6 +118,7 @@ export async function runTui(options: RunTuiOptions): Promise<void> {
117
118
  ),
118
119
  toolingConfig: options.publicConfig.tooling,
119
120
  enableTurnUndo: true,
121
+ enableAskUser: true,
120
122
  bashGuard: {
121
123
  mode: sessionConfig.bashGuardMode,
122
124
  source: sessionConfig.bashGuardSource,
@@ -133,6 +135,18 @@ export async function runTui(options: RunTuiOptions): Promise<void> {
133
135
  workspaceRoot,
134
136
  sessionId,
135
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
+ }),
136
150
  completedTurnHook: memoryCoordinator,
137
151
  }),
138
152
  };
@@ -286,6 +300,7 @@ export async function runTui(options: RunTuiOptions): Promise<void> {
286
300
  instance = render(
287
301
  <App
288
302
  sessionController={controller}
303
+ version={options.version}
289
304
  readGitBranch={readCurrentGitBranch}
290
305
  history={promptHistory}
291
306
  projectSlashCommands={projectSlashCommands}
@@ -321,7 +336,6 @@ export async function runTui(options: RunTuiOptions): Promise<void> {
321
336
  } finally {
322
337
  instance?.unmount();
323
338
  restoreStdin();
324
- memoryCoordinator?.dispose();
325
339
  if (controller !== undefined) {
326
340
  try {
327
341
  await controller.dispose(disposeReason);
@@ -335,6 +349,7 @@ export async function runTui(options: RunTuiOptions): Promise<void> {
335
349
  );
336
350
  }
337
351
  }
352
+ memoryCoordinator?.dispose();
338
353
  }
339
354
 
340
355
  if (primaryError !== undefined) {
@@ -140,6 +140,16 @@ export class StdoutEventPrinter implements EventSink {
140
140
  `tool.confirmation.resolved toolCallId=${event.toolCallId} decision=${event.data.decision} durationMs=${event.data.durationMs}\n`,
141
141
  );
142
142
  break;
143
+ case "tool.user_question.requested":
144
+ this.stdout.write(
145
+ `tool.user_question.requested toolCallId=${event.toolCallId} question=${JSON.stringify(event.data.question)} options=${event.data.options.length}\n`,
146
+ );
147
+ break;
148
+ case "tool.user_question.resolved":
149
+ this.stdout.write(
150
+ `tool.user_question.resolved toolCallId=${event.toolCallId} outcome=${event.data.outcome} durationMs=${event.data.durationMs}\n`,
151
+ );
152
+ break;
143
153
  case "mcp.server.connected":
144
154
  this.stdout.write(
145
155
  `mcp.server.connected name=${event.data.serverName} tools=${event.data.toolCount}\n`,
@@ -235,7 +245,11 @@ function formatToolRawResult(call: ToolCall, raw: ToolRawResult): string[] {
235
245
  case "context_maintenance":
236
246
  case "memory_search":
237
247
  case "memory_get":
248
+ case "memory_create":
249
+ case "memory_update":
250
+ case "memory_delete":
238
251
  case "wait":
252
+ case "ask_user":
239
253
  case "mcp":
240
254
  case "generic":
241
255
  return [];
@@ -351,6 +351,14 @@ export type AgentEventDataMap = {
351
351
  decision: "allow" | "deny" | "cancelled";
352
352
  durationMs: number;
353
353
  };
354
+ "tool.user_question.requested": {
355
+ question: string;
356
+ options: readonly { description: string }[];
357
+ };
358
+ "tool.user_question.resolved":
359
+ | { outcome: "selected"; answer: string; durationMs: number }
360
+ | { outcome: "dismissed"; durationMs: number }
361
+ | { outcome: "cancelled"; durationMs: number };
354
362
  "agent.iteration.finished": {
355
363
  outcome: "continue" | "completed";
356
364
  toolCallCount: number;
@@ -448,6 +456,7 @@ export type AgentEventInput =
448
456
  "tool.started" | "tool.raw_result" | "tool.finished" | "tool.observation"
449
457
  >
450
458
  | ToolEventInput<"tool.confirmation.requested" | "tool.confirmation.resolved">
459
+ | ToolEventInput<"tool.user_question.requested" | "tool.user_question.resolved">
451
460
  | ToolEventInput<
452
461
  "bash.task.backgrounded" | "bash.task.stopping" | "bash.task.finished"
453
462
  >;
@@ -2,6 +2,9 @@ import type { SessionId, TurnId } from "../ids/runtime-id";
2
2
 
3
3
  export const MEMORY_SEARCH_TOOL_NAME = "MemorySearch" as const;
4
4
  export const MEMORY_GET_TOOL_NAME = "MemoryGet" as const;
5
+ export const MEMORY_CREATE_TOOL_NAME = "MemoryCreate" as const;
6
+ export const MEMORY_UPDATE_TOOL_NAME = "MemoryUpdate" as const;
7
+ export const MEMORY_DELETE_TOOL_NAME = "MemoryDelete" as const;
5
8
  export const MEMORY_SCHEMA_VERSION = 2 as const;
6
9
  export const MAX_MEMORY_TEXT_BYTES = 512;
7
10
  export const MAX_MEMORY_SUMMARY_BYTES = 4_096;
@@ -112,6 +115,35 @@ export type StoredMemoryRecord = StoredMemorySummary & {
112
115
  readonly sourceTurnId: string;
113
116
  };
114
117
 
118
+ export type StoredMemoryMutationRecord = StoredMemoryRecord & {
119
+ readonly embedding: Float32Array;
120
+ };
121
+
122
+ export type MemoryUpdateStoreResult =
123
+ | {
124
+ readonly ok: true;
125
+ readonly memoryId: string;
126
+ }
127
+ | {
128
+ readonly ok: false;
129
+ readonly code: "memory_not_found";
130
+ }
131
+ | {
132
+ readonly ok: false;
133
+ readonly code: "memory_duplicate";
134
+ readonly conflictMemoryId: string;
135
+ };
136
+
137
+ export type MemoryDeleteStoreResult =
138
+ | {
139
+ readonly ok: true;
140
+ readonly memoryId: string;
141
+ }
142
+ | {
143
+ readonly ok: false;
144
+ readonly code: "memory_not_found";
145
+ };
146
+
115
147
  export type MemoryExtractionRejectedCounts = {
116
148
  readonly duplicate: number;
117
149
  readonly secret: number;
@@ -175,10 +207,24 @@ export type MemoryGetDiagnostic = {
175
207
  readonly ms: number;
176
208
  };
177
209
 
210
+ export type MemoryMutationDiagnostic = {
211
+ readonly at: string;
212
+ readonly kind: "create" | "update" | "delete";
213
+ readonly outcome: "ok" | "failed" | "skipped";
214
+ readonly reason: string | null;
215
+ readonly workspace: string;
216
+ readonly sessionId: string;
217
+ readonly turnId: string;
218
+ readonly toolCallId: string;
219
+ readonly memoryId: string | null;
220
+ readonly ms: number;
221
+ };
222
+
178
223
  export type MemoryDiagnostic =
179
224
  | MemoryExtractionDiagnostic
180
225
  | MemorySearchDiagnostic
181
226
  | MemoryGetDiagnostic
227
+ | MemoryMutationDiagnostic
182
228
  | MemoryInitDiagnostic;
183
229
 
184
230
  export class MemoryError extends Error {