tinker-agent 2.11.0 → 2.13.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 +32 -1
  2. package/README.md +31 -67
  3. package/package.json +1 -1
  4. package/src/agent/runtime-session-contracts.ts +2 -29
  5. package/src/agent/runtime-session.ts +13 -67
  6. package/src/cli/config.ts +0 -29
  7. package/src/cli/model-profiles.ts +0 -80
  8. package/src/cli/public-config-contract.ts +1 -82
  9. package/src/cli/runner-dependencies.ts +35 -35
  10. package/src/cli/tui-runner.tsx +2 -41
  11. package/src/events/observation-text-log.ts +7 -0
  12. package/src/memory/contracts.ts +0 -256
  13. package/src/memory/memory-create-tool.ts +3 -5
  14. package/src/memory/memory-files.ts +145 -0
  15. package/src/memory/memory-search-output.ts +23 -0
  16. package/src/memory/memory-search-tool.ts +79 -175
  17. package/src/memory/memory-search.ts +115 -0
  18. package/src/observation/observation-builder.ts +5 -1
  19. package/src/session/session-store-contracts.ts +0 -23
  20. package/src/session/session-store.ts +2 -103
  21. package/src/tools/bash.ts +9 -1
  22. package/src/tools/bounded-output-preview.ts +2 -2
  23. package/src/tools/registry.ts +15 -18
  24. package/src/tools/types.ts +12 -0
  25. package/src/tui/app.tsx +6 -4
  26. package/src/tui/event-store.ts +3 -1
  27. package/src/cli/tui-memory.ts +0 -69
  28. package/src/memory/embedding-client.ts +0 -105
  29. package/src/memory/memory-coordinator.ts +0 -1274
  30. package/src/memory/memory-delete-tool.ts +0 -88
  31. package/src/memory/memory-extractor.ts +0 -252
  32. package/src/memory/memory-get-tool.ts +0 -86
  33. package/src/memory/memory-log.ts +0 -88
  34. package/src/memory/memory-store.ts +0 -1133
  35. package/src/memory/memory-update-tool.ts +0 -142
  36. package/src/memory/vector.ts +0 -153
@@ -12,47 +12,47 @@ import type { RunnerConfig } from "./config";
12
12
 
13
13
  export const RUNTIME_INSTRUCTIONS = (
14
14
  workspaceRoot: string,
15
- ): string => `You are a coding agent. Your name is Tinker.
15
+ ): string => `You are Tinker, a coding agent.
16
+
17
+ ## Workspace
16
18
 
17
- Current workspace:
18
19
  ${workspaceRoot}
19
20
 
20
- Use this path as the root for relative file paths. Absolute file paths may point outside this workspace.
21
-
22
- You can use tools to find, read, edit, write files, and run shell commands.
23
- Use Glob to find files by name or path pattern.
24
- Use Grep to search file contents.
25
- With Grep, start with output_mode="files_with_matches" to narrow scope, then use output_mode="content" when you need matching lines.
26
- Use head_limit and offset to page through large Grep result sets instead of requesting unlimited output.
27
- Use Read to open specific files returned by Grep.
28
- Use Edit to replace exact strings in existing files. Set old_string="" to create a file or write to an empty file.
29
- Use Read before the first Write of an existing file in the current runtime.
30
- Write creates missing parent directories when creating a file.
31
- Write may fail if the runtime has no known version or the file changed after it was last observed. If that happens, call Read again and retry with the updated content.
32
- Use Read before an exact-string Edit when this runtime has not already established the current version through Read, Write, or Edit. A successful paginated Read is sufficient. Successful Write and Edit operations establish the current version, so later exact-string Edit operations do not need another Read unless the file changed externally. Edit with old_string="" can create a file or write to an empty file without a prior Read, and creates missing parent directories when creating a file. Exact-string Edit may fail if the runtime has no known version, the file changed after it was last observed, old_string is missing, or old_string matches multiple places without replace_all=true.
33
- Use WebSearch, when it is available, to look up current information on the web such as recent releases, documentation, and news. Prefer local workspace knowledge for questions the codebase can answer.
34
- Use WebFetch to read the content of a specific URL, such as documentation pages found via WebSearch.
21
+ Relative file-tool paths resolve from this workspace.
22
+ Absolute file paths may refer to locations outside it.
23
+
24
+ ## Runtime contracts
25
+
26
+ Read, Write, and Edit participate in runtime file-version tracking.
27
+ Their tool definitions specify operation-specific preconditions and exceptions.
35
28
  Prefer Read for reading files instead of using cat on large files.
36
29
  Prefer Write or Edit for changing files instead of shell redirection.
37
- Use run_in_background=true for persistent processes such as dev servers and watch commands, or when you have independent work to do while a command runs.
38
- For finite commands whose result is needed next, such as builds, tests, and checks, prefer foreground execution when no independent work remains. Set a sufficient foreground timeout; the call returns as soon as the command finishes.
39
- Do not add & to Bash commands; background execution is handled by the Bash tool.
40
- Use Bash with tty=true for REPLs, debuggers, interactive prompts, and terminal applications that require a controlling terminal.
41
- TaskList lists background shell tasks in the current session.
42
- TaskOutput reports a task's current status, latest output, or current terminal screen. For non-PTY logs, offset (1-based) and limit select consecutive lines instead of the default head/tail preview; PTY tasks ignore them. Range truncated=true means byte limits shortened requested content, not that lines outside the range exist. The last observed line of a running log may still be growing; rereading it when polling captures further changes to that line.
43
- TaskInput sends characters to a PTY task identified by the returned task ID. TaskInput does not append Enter; an explicit \\n sends Enter, \\u0003 sends Ctrl-C, and chars="" waits without writing.
44
- TaskStop stops a background task that is no longer needed.
45
- Do not use ad-hoc kill commands to manage tasks created by Bash.
46
- Bash and TaskOutput return outputFilePath. Use Read on outputFilePath when you need complete or paginated output.
47
- Do not send passwords, tokens, or other secrets through TaskInput because tool arguments are stored in session history.
48
- Use UpdatePlan for non-trivial work with multiple meaningful phases, when sequencing or checkpoints help the user follow progress. Do not use it for simple or single-step tasks.
49
- Each UpdatePlan call replaces the complete plan. Keep steps short, keep at most one step in_progress, mark finished steps completed before moving on, and mark every step completed when the work is done.
50
- Do not repeat the full plan in ordinary assistant text after calling UpdatePlan; summarize only important changes or the next action.
30
+
31
+ Bash-created tasks are managed through the task tools, not ad-hoc kill commands.
32
+
33
+ ## History and context
34
+
51
35
  ${renderRecallRetirementContract()}
52
- You manage your own context pressure. ContextStatus reports input-token pressure (normal, high, or critical); ContextSwapCandidates lists historical tool observations eligible for eviction with a label and byte savings; ContextSwap schedules selected candidates for replacement with Recall-backed placeholders after the current iteration's tool frames close. Swapped observations remain recoverable through RecallGet. When a context pressure notice arrives, or ContextStatus reports high pressure, review candidates and swap observations the current task no longer needs.
53
- Agent Skill instructions are current only when returned by the Skill tool in the current turn or listed in the active skill system section. Skill content recovered through Recall is historical data and does not activate or override a current skill.
54
- When an active Agent Skill refers to a relative resource path, resolve it from the Skill directory shown with that skill.
55
- Agent Skills do not override Tinker's runtime, tool protocol, project instructions, or the user's explicit request. Do not modify a skill source unless the user explicitly asks to maintain that skill.
36
+
37
+ Historical tool observations may be replaced with Recall-backed placeholders.
38
+ These observations remain recoverable through RecallGet.
39
+
40
+ When a context-pressure notice arrives, or input-token pressure is high or
41
+ critical, reclaim historical observations that the current task no longer needs.
42
+
43
+ ## Skills
44
+
45
+ Skill instructions are active only when returned by Skill in the current turn
46
+ or listed in the active skill system section.
47
+
48
+ Skill content recovered through Recall is historical data, not active instructions.
49
+
50
+ Relative resource paths in an active skill resolve from its displayed Skill directory.
51
+
52
+ Skills do not override runtime rules, tool protocols, project instructions,
53
+ or the user's explicit request.
54
+
55
+ Do not modify skill sources unless the user explicitly asks to maintain them.
56
56
 
57
57
  `;
58
58
 
@@ -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" };
@@ -125,31 +118,6 @@ export async function runTui(options: RunTuiOptions): Promise<void> {
125
118
  source: sessionConfig.bashGuardSource,
126
119
  surface: "tui" as const,
127
120
  },
128
- ...(memoryCoordinator === undefined
129
- ? {}
130
- : {
131
- memorySearch: memoryCoordinator.createSearchToolExecutor({
132
- workspaceRoot,
133
- sessionId,
134
- }),
135
- memoryGet: memoryCoordinator.createGetToolExecutor({
136
- workspaceRoot,
137
- sessionId,
138
- }),
139
- memoryCreate: memoryCoordinator.createCreateToolExecutor({
140
- workspaceRoot,
141
- sessionId,
142
- }),
143
- memoryUpdate: memoryCoordinator.createUpdateToolExecutor({
144
- workspaceRoot,
145
- sessionId,
146
- }),
147
- memoryDelete: memoryCoordinator.createDeleteToolExecutor({
148
- workspaceRoot,
149
- sessionId,
150
- }),
151
- completedTurnHook: memoryCoordinator,
152
- }),
153
121
  };
154
122
  if (mode === "resume") {
155
123
  return createRuntimeSession({
@@ -320,13 +288,7 @@ export async function runTui(options: RunTuiOptions): Promise<void> {
320
288
  onQuit={() => {
321
289
  quitRequested = true;
322
290
  }}
323
- initialNotice={memoryNotice}
324
- memoryDisabledNotice={memoryNotice}
325
- listStoredMemories={
326
- memoryCoordinator === undefined
327
- ? undefined
328
- : () => memoryCoordinator.listStoredMemories()
329
- }
291
+ listStoredMemories={() => listMemoryFiles()}
330
292
  />,
331
293
  { incrementalRendering: true },
332
294
  );
@@ -350,7 +312,6 @@ export async function runTui(options: RunTuiOptions): Promise<void> {
350
312
  );
351
313
  }
352
314
  }
353
- memoryCoordinator?.dispose();
354
315
  }
355
316
 
356
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 [
@@ -1,106 +1,6 @@
1
- import type { SessionId, TurnId } from "../ids/runtime-id";
2
-
3
- export const MEMORY_SEARCH_TOOL_NAME = "MemorySearch" as const;
4
- export const MEMORY_GET_TOOL_NAME = "MemoryGet" as const;
5
1
  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;
8
- export const MEMORY_SCHEMA_VERSION = 2 as const;
9
2
  export const MAX_MEMORY_TEXT_BYTES = 512;
10
3
  export const MAX_MEMORY_SUMMARY_BYTES = 4_096;
11
- export const MAX_SEARCH_RESULT_SUMMARY_BYTES = 1_536;
12
- export const MAX_MEMORY_QUERY_BYTES = 1_024;
13
- export const MAX_MEMORY_ID_BYTES = 64;
14
- export const MEMORY_SEARCH_LIMIT = 5;
15
- export const MEMORY_RECALL_CANDIDATE_LIMIT = 20;
16
- export const MAX_MEMORY_KEYWORDS = 8;
17
- export const MAX_MEMORY_KEYWORD_BYTES = 128;
18
- export const MEMORY_RRF_K = 60;
19
- export const MEMORY_EXTRACTION_QUEUE_CAPACITY = 64;
20
- export const MEMORY_SEARCH_DIAGNOSTIC_VECTOR_SCORES_LIMIT = 10;
21
-
22
- export type MemoryEmbeddingKind = "openai-compatible";
23
-
24
- export type MemoryEmbeddingConfig = {
25
- readonly name: string;
26
- readonly kind: MemoryEmbeddingKind;
27
- readonly model: string;
28
- readonly apiBase: string;
29
- readonly apiKey: string;
30
- readonly dimensions: number;
31
- };
32
-
33
- export type MemoryEmbeddingIdentity = Pick<
34
- MemoryEmbeddingConfig,
35
- "name" | "kind" | "model" | "dimensions"
36
- >;
37
-
38
- export type MemoryPaths = {
39
- readonly directory: string;
40
- readonly database: string;
41
- readonly log: string;
42
- readonly extractedLog: string;
43
- };
44
-
45
- export type MemoryWriteCandidate = {
46
- readonly text: string;
47
- readonly summary: string;
48
- readonly embedding: Float32Array;
49
- };
50
-
51
- export type MemoryWriteBatch = {
52
- readonly workspaceRoot: string;
53
- readonly sessionId: SessionId;
54
- readonly turnId: TurnId;
55
- readonly candidates: readonly MemoryWriteCandidate[];
56
- };
57
-
58
- export type MemoryWriteResult = {
59
- readonly written: number;
60
- readonly duplicate: number;
61
- readonly inserted: readonly MemoryInsertedRecord[];
62
- };
63
-
64
- export type MemoryInsertedRecord = {
65
- readonly memoryId: string;
66
- readonly text: string;
67
- readonly createdAt: string;
68
- };
69
-
70
- export type MemorySearchMatch = {
71
- readonly memoryId: string;
72
- readonly text: string;
73
- readonly summary: string;
74
- readonly score: number;
75
- readonly sourceWorkspace: string;
76
- readonly sourceSessionId: string;
77
- readonly createdAt: string;
78
- };
79
-
80
- export type MemoryRecallPath = "vector" | "fts";
81
-
82
- export type MemoryFtsMatch = {
83
- readonly memoryId: string;
84
- readonly text: string;
85
- readonly summary: string;
86
- readonly bm25: number;
87
- readonly sourceWorkspace: string;
88
- readonly sourceSessionId: string;
89
- readonly createdAt: string;
90
- };
91
-
92
- export type MemoryHybridMatch = {
93
- readonly memoryId: string;
94
- readonly text: string;
95
- readonly summary: string;
96
- readonly score: number;
97
- readonly via: readonly MemoryRecallPath[];
98
- readonly sourceWorkspace: string;
99
- readonly sourceSessionId: string;
100
- readonly createdAt: string;
101
- };
102
-
103
- export type MemoryRecallDegraded = "vector" | "fts";
104
4
 
105
5
  export type StoredMemorySummary = {
106
6
  readonly memoryId: string;
@@ -111,168 +11,12 @@ export type StoredMemorySummary = {
111
11
  readonly createdAt: string;
112
12
  };
113
13
 
114
- export type StoredMemoryRecord = StoredMemorySummary & {
115
- readonly sourceTurnId: string;
116
- };
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
-
147
- export type MemoryExtractionRejectedCounts = {
148
- readonly duplicate: number;
149
- readonly secret: number;
150
- readonly invalid: number;
151
- readonly embedding: number;
152
- };
153
-
154
- export type MemoryExtractionDiagnostic = {
155
- readonly at: string;
156
- readonly kind: "extraction";
157
- readonly outcome: "ok" | "failed" | "skipped";
158
- readonly reason: string | null;
159
- readonly workspace: string;
160
- readonly turnId: string;
161
- readonly inputTokens: number;
162
- readonly returned: number;
163
- readonly written: number;
164
- readonly rejected: MemoryExtractionRejectedCounts;
165
- readonly ms: number;
166
- /**
167
- * Bounded single-line error detail (message plus cause chain) recorded for
168
- * failed and skipped outcomes so provider/parse failures are diagnosable
169
- * from the log alone. Absent on success.
170
- */
171
- readonly detail?: string;
172
- };
173
-
174
- export type MemorySearchDiagnostic = {
175
- readonly at: string;
176
- readonly kind: "search";
177
- readonly outcome: "ok" | "failed" | "skipped";
178
- readonly reason: string | null;
179
- readonly workspace: string;
180
- readonly sessionId: string;
181
- readonly queryBytes: number;
182
- readonly keywordCount: number;
183
- readonly returned: number;
184
- readonly vectorReturned: number;
185
- readonly ftsReturned: number;
186
- readonly degraded: MemoryRecallDegraded | null;
187
- readonly scores: readonly number[];
188
- readonly vectorScores: readonly number[];
189
- readonly ms: number;
190
- };
191
-
192
- export type MemoryInitDiagnostic = {
193
- readonly at: string;
194
- readonly kind: "init";
195
- readonly outcome: "failed";
196
- readonly reason: string;
197
- };
198
-
199
- export type MemoryGetDiagnostic = {
200
- readonly at: string;
201
- readonly kind: "get";
202
- readonly outcome: "ok" | "failed";
203
- readonly reason: string | null;
204
- readonly workspace: string;
205
- readonly sessionId: string;
206
- readonly found: boolean;
207
- readonly ms: number;
208
- };
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
-
223
- export type MemoryDiagnostic =
224
- | MemoryExtractionDiagnostic
225
- | MemorySearchDiagnostic
226
- | MemoryGetDiagnostic
227
- | MemoryMutationDiagnostic
228
- | MemoryInitDiagnostic;
229
-
230
- export class MemoryError extends Error {
231
- constructor(
232
- readonly code: string,
233
- message: string,
234
- options?: ErrorOptions,
235
- ) {
236
- super(message, options);
237
- this.name = "MemoryError";
238
- }
239
- }
240
-
241
- export function memoryErrorCode(error: unknown, fallback: string): string {
242
- return error instanceof MemoryError ? error.code : fallback;
243
- }
244
-
245
14
  export function boundedMemoryError(error: unknown): string {
246
15
  const raw = error instanceof Error ? error.message : String(error);
247
16
  const singleLine = raw.replaceAll(/\s+/g, " ").trim() || "unknown memory error";
248
17
  return truncateUtf8(singleLine, 400);
249
18
  }
250
19
 
251
- export function boundedMemoryErrorDetail(error: unknown): string {
252
- const parts: string[] = [];
253
- let current: unknown = error;
254
- for (
255
- let depth = 0;
256
- depth < 4 && current !== undefined && current !== null;
257
- depth += 1
258
- ) {
259
- const raw =
260
- current instanceof Error
261
- ? current.message
262
- : typeof current === "string" ||
263
- typeof current === "number" ||
264
- typeof current === "boolean"
265
- ? String(current)
266
- : "unknown non-error cause";
267
- const singleLine = raw.replaceAll(/\s+/g, " ").trim();
268
- if (singleLine !== "" && !parts.includes(singleLine)) {
269
- parts.push(singleLine);
270
- }
271
- current = current instanceof Error ? current.cause : undefined;
272
- }
273
- return truncateUtf8(parts.join(" | ") || "unknown memory error", 400);
274
- }
275
-
276
20
  export function truncateUtf8(value: string, maxBytes: number): string {
277
21
  if (Buffer.byteLength(value, "utf8") <= maxBytes) {
278
22
  return value;
@@ -14,7 +14,8 @@ import {
14
14
 
15
15
  export const MEMORY_CREATE_TOOL_DEFINITION: ToolDefinition = Object.freeze({
16
16
  name: MEMORY_CREATE_TOOL_NAME,
17
- description: "Create one global memory shared across sessions and workspaces.",
17
+ description:
18
+ "Save an explicit memory as a Markdown note shared across sessions and workspaces. Returns a file path that Read can open.",
18
19
  parameters: {
19
20
  type: "object",
20
21
  additionalProperties: false,
@@ -23,7 +24,7 @@ export const MEMORY_CREATE_TOOL_DEFINITION: ToolDefinition = Object.freeze({
23
24
  type: "string",
24
25
  minLength: 1,
25
26
  maxLength: MAX_MEMORY_TEXT_BYTES,
26
- description: "A one-line searchable index for this memory.",
27
+ description: "A one-line Markdown heading for this memory.",
27
28
  },
28
29
  summary: {
29
30
  type: "string",
@@ -43,15 +44,12 @@ export function createMemoryCreateToolExecutor(options: {
43
44
  call: ToolCall,
44
45
  signal: AbortSignal,
45
46
  ) => Promise<MemoryCreateRawResult>;
46
- readonly recordInvalidCall: (call: ToolCall) => Promise<void>;
47
47
  }): ToolExecutor {
48
48
  return defineToolExecutor("memory_create", {
49
49
  definition: MEMORY_CREATE_TOOL_DEFINITION,
50
50
  async execute(args, call, context): Promise<MemoryCreateRawResult> {
51
51
  const parsed = parseMemoryCreateArgs(args);
52
52
  if (!parsed.ok) {
53
- throwIfTurnCancelled(context.signal);
54
- await options.recordInvalidCall(call);
55
53
  throwIfTurnCancelled(context.signal);
56
54
  return { ok: false, error: parsed.error };
57
55
  }
@@ -0,0 +1,145 @@
1
+ import { defaultHomeRoot } from "../session/workspace-storage";
2
+ import path from "node:path";
3
+ import { access, mkdir, open, readdir, rename, writeFile } from "node:fs/promises";
4
+ import { createUuidV7 } from "../ids/uuid-v7";
5
+ import { throwIfTurnCancelled } from "../agent/turn-cancellation";
6
+ import type { ToolExecutor } from "../tools/types";
7
+ import { boundedMemoryError, type StoredMemorySummary } from "./contracts";
8
+ import { createMemoryCreateToolExecutor } from "./memory-create-tool";
9
+
10
+ export function memoryDirectory(homeRoot = defaultHomeRoot()): string {
11
+ return path.resolve(homeRoot, ".tinker", "memory");
12
+ }
13
+
14
+ export function sessionMemoryPath(sessionId: string, homeRoot?: string): string {
15
+ return path.join(memoryDirectory(homeRoot), "records", `${sessionId}.md`);
16
+ }
17
+
18
+ /** Old database and diagnostic files are retained outside the searchable directory. */
19
+ export async function ensureMemoryDirectory(homeRoot?: string): Promise<string> {
20
+ const directory = memoryDirectory(homeRoot);
21
+ await mkdir(directory, { recursive: true, mode: 0o700 });
22
+ const legacy = (await readdir(directory)).filter((name) =>
23
+ [
24
+ "memory.sqlite",
25
+ "memory.sqlite-wal",
26
+ "memory.sqlite-shm",
27
+ "memory-log.jsonl",
28
+ "extracted-memories.log",
29
+ ].includes(name),
30
+ );
31
+ if (legacy.length > 0) {
32
+ const archive = path.join(path.dirname(directory), "memory-legacy", createUuidV7());
33
+ await mkdir(archive, { recursive: true, mode: 0o700 });
34
+ for (const name of legacy) {
35
+ await rename(path.join(directory, name), path.join(archive, name)).catch(
36
+ (error: unknown) => {
37
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
38
+ },
39
+ );
40
+ }
41
+ }
42
+ return directory;
43
+ }
44
+
45
+ export function createFileMemoryCreateToolExecutor(input: {
46
+ workspaceRoot: string;
47
+ homeRoot?: string;
48
+ clock?: () => string;
49
+ }): ToolExecutor {
50
+ return createMemoryCreateToolExecutor({
51
+ async create(text, summary, _call, signal) {
52
+ throwIfTurnCancelled(signal);
53
+ try {
54
+ const directory = path.join(
55
+ await ensureMemoryDirectory(input.homeRoot),
56
+ "notes",
57
+ );
58
+ await mkdir(directory, { recursive: true, mode: 0o700 });
59
+ const memoryId = createUuidV7();
60
+ const createdAt = (input.clock ?? (() => new Date().toISOString()))();
61
+ const filePath = path.join(directory, `${memoryId}.md`);
62
+ const title = text.replaceAll(/\s*\r?\n\s*/g, " ");
63
+ const heading = title.startsWith("# ") ? title : `# ${title}`;
64
+ const markdown = `${heading}\n\nCreated: ${createdAt}\nWorkspace: ${input.workspaceRoot}\n${summary === "" ? "" : `\n${summary}\n`}`;
65
+ throwIfTurnCancelled(signal);
66
+ await writeFile(filePath, markdown, { flag: "wx", mode: 0o600 });
67
+ return { ok: true, status: "created", memoryId, createdAt, filePath };
68
+ } catch (error) {
69
+ throwIfTurnCancelled(signal);
70
+ return { ok: false, error: boundedMemoryError(error) };
71
+ }
72
+ },
73
+ });
74
+ }
75
+
76
+ /** The browser shows a bounded preview; Read can open the complete file. */
77
+ export async function listMemoryFiles(
78
+ homeRoot?: string,
79
+ ): Promise<readonly StoredMemorySummary[]> {
80
+ const directory = await ensureMemoryDirectory(homeRoot);
81
+ const memories: StoredMemorySummary[] = [];
82
+ for (const folder of ["notes", "records"]) {
83
+ const entries = await readdir(path.join(directory, folder), {
84
+ withFileTypes: true,
85
+ }).catch((error: unknown) => {
86
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") return [];
87
+ throw error;
88
+ });
89
+ for (const entry of entries) {
90
+ if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
91
+ const filePath = path.join(directory, folder, entry.name);
92
+ const handle = await open(filePath, "r");
93
+ let content: string;
94
+ try {
95
+ const buffer = Buffer.alloc(4_096);
96
+ const { bytesRead } = await handle.read(buffer, 0, buffer.length, 0);
97
+ content = buffer.subarray(0, bytesRead).toString("utf8");
98
+ } finally {
99
+ await handle.close();
100
+ }
101
+ const lines = content.split("\n");
102
+ const id = entry.name.slice(0, -3);
103
+ memories.push({
104
+ memoryId: id,
105
+ text: lines[0]?.replace(/^# /, "") ?? entry.name,
106
+ summary: lines
107
+ .slice(1)
108
+ .filter(
109
+ (line) => !/^(Created|Started|Workspace|Model|Max iterations): /.test(line),
110
+ )
111
+ .join("\n")
112
+ .trim(),
113
+ sourceWorkspace: /^Workspace: (.*)$/m.exec(content)?.[1] ?? "",
114
+ sourceSessionId: folder === "records" ? id : "",
115
+ createdAt: /^(?:Created|Started): (.*)$/m.exec(content)?.[1] ?? "",
116
+ });
117
+ }
118
+ }
119
+ return memories.sort(
120
+ (a, b) =>
121
+ b.createdAt.localeCompare(a.createdAt) || b.memoryId.localeCompare(a.memoryId),
122
+ );
123
+ }
124
+
125
+ /** Move an older session-local transcript when reopening or publishing a clone. */
126
+ export async function prepareSessionMemory(
127
+ sessionDirectory: string,
128
+ sessionId: string,
129
+ homeRoot?: string,
130
+ ): Promise<void> {
131
+ const directory = await ensureMemoryDirectory(homeRoot);
132
+ await mkdir(path.join(directory, "records"), { recursive: true, mode: 0o700 });
133
+ const target = sessionMemoryPath(sessionId, homeRoot);
134
+ try {
135
+ await access(target);
136
+ return;
137
+ } catch (error) {
138
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
139
+ }
140
+ await rename(path.join(sessionDirectory, "observations.md"), target).catch(
141
+ (error: unknown) => {
142
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
143
+ },
144
+ );
145
+ }
@@ -0,0 +1,23 @@
1
+ import type { MemoryTextSearchResult } from "../tools/types";
2
+
3
+ export function renderMemoryTextSearch(result: MemoryTextSearchResult): string {
4
+ const sections = result.files.map(({ filePath, lines }) => {
5
+ const path = /[\p{Cc}\p{Cf}"\\\u2028\u2029]/u.test(filePath)
6
+ ? JSON.stringify(filePath)
7
+ : filePath;
8
+ const output = [`File: ${path}`, ""];
9
+ let previous: number | undefined;
10
+ for (const line of lines) {
11
+ if (previous !== undefined && line.lineNumber > previous + 1)
12
+ output.push("", "…", "");
13
+ output.push(`${line.match ? ">" : " "} ${line.lineNumber} | ${line.text}`);
14
+ previous = line.lineNumber;
15
+ }
16
+ return output.join("\n");
17
+ });
18
+ if (sections.length === 0) sections.push("No matches found on this page.");
19
+ if (result.hasMore)
20
+ sections.push(`More results available; nextOffset=${result.nextOffset}.`);
21
+ else sections.push("End of results.");
22
+ return sections.join("\n\n");
23
+ }