@letta-ai/letta-code 0.34.25 → 0.34.27

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 (156) hide show
  1. package/cursor-worker.js +168 -0
  2. package/dist/agent-presets.js +5 -68
  3. package/dist/agent-presets.js.map +2 -2
  4. package/dist/channels-slack.js +30 -24
  5. package/dist/channels-slack.js.map +3 -3
  6. package/dist/mcp-client.js +233 -75
  7. package/dist/mcp-client.js.map +9 -9
  8. package/dist/mcp-oauth.js +257 -89
  9. package/dist/mcp-oauth.js.map +12 -11
  10. package/dist/memory-confinement.js +109 -51
  11. package/dist/memory-confinement.js.map +9 -8
  12. package/dist/types/agent/mcp-originator-authority-file.d.ts +1 -0
  13. package/dist/types/agent/mcp-originator-authority-file.d.ts.map +1 -1
  14. package/dist/types/agent/memory-filesystem.d.ts +3 -3
  15. package/dist/types/agent/memory-filesystem.d.ts.map +1 -1
  16. package/dist/types/agent/memory-git.d.ts +2 -2
  17. package/dist/types/agent/memory-git.d.ts.map +1 -1
  18. package/dist/types/agent/skills.d.ts +3 -3
  19. package/dist/types/agent/skills.d.ts.map +1 -1
  20. package/dist/types/agent/subagents/index.d.ts +8 -0
  21. package/dist/types/agent/subagents/index.d.ts.map +1 -1
  22. package/dist/types/agent/subagents/manager.d.ts.map +1 -1
  23. package/dist/types/agent/subagents/memory-worker.d.ts +2 -8
  24. package/dist/types/agent/subagents/memory-worker.d.ts.map +1 -1
  25. package/dist/types/agent/subagents/subagent-launcher.d.ts.map +1 -1
  26. package/dist/types/backend/api/providers.d.ts +8 -2
  27. package/dist/types/backend/api/providers.d.ts.map +1 -1
  28. package/dist/types/backend/api/schedules.d.ts +16 -2
  29. package/dist/types/backend/api/schedules.d.ts.map +1 -1
  30. package/dist/types/backend/backend.d.ts +5 -2
  31. package/dist/types/backend/backend.d.ts.map +1 -1
  32. package/dist/types/backend/dev/fake-headless-backend.d.ts +2 -1
  33. package/dist/types/backend/dev/fake-headless-backend.d.ts.map +1 -1
  34. package/dist/types/backend/dev/pi-model-factory.d.ts.map +1 -1
  35. package/dist/types/backend/local/local-provider-auth-store.d.ts +2 -0
  36. package/dist/types/backend/local/local-provider-auth-store.d.ts.map +1 -1
  37. package/dist/types/backend/local/local-transcript-history.d.ts +10 -0
  38. package/dist/types/backend/local/local-transcript-history.d.ts.map +1 -0
  39. package/dist/types/backend/local/local-transcript.d.ts +6 -0
  40. package/dist/types/backend/local/local-transcript.d.ts.map +1 -1
  41. package/dist/types/channels/slack/attachment-primitives.d.ts +2 -1
  42. package/dist/types/channels/slack/attachment-primitives.d.ts.map +1 -1
  43. package/dist/types/channels/types.d.ts +1 -1
  44. package/dist/types/cli/helpers/chunk-log.d.ts.map +1 -1
  45. package/dist/types/cli/subcommands/mcp.d.ts +2 -2
  46. package/dist/types/cli/subcommands/mcp.d.ts.map +1 -1
  47. package/dist/types/cron/cron-file.d.ts.map +1 -1
  48. package/dist/types/cron/process-identity.d.ts.map +1 -1
  49. package/dist/types/cron/run-log.d.ts +1 -0
  50. package/dist/types/cron/run-log.d.ts.map +1 -1
  51. package/dist/types/headless-environment-response.d.ts +27 -0
  52. package/dist/types/headless-environment-response.d.ts.map +1 -1
  53. package/dist/types/hooks/loader.d.ts.map +1 -1
  54. package/dist/types/mods/paths.d.ts +3 -3
  55. package/dist/types/mods/paths.d.ts.map +1 -1
  56. package/dist/types/permissions/agent-memory-prefixes.d.ts.map +1 -1
  57. package/dist/types/permissions/analyzer.d.ts +1 -0
  58. package/dist/types/permissions/analyzer.d.ts.map +1 -1
  59. package/dist/types/permissions/cross-agent-guard.d.ts.map +1 -1
  60. package/dist/types/permissions/loader.d.ts +2 -1
  61. package/dist/types/permissions/loader.d.ts.map +1 -1
  62. package/dist/types/permissions/sandbox-policy.d.ts +9 -7
  63. package/dist/types/permissions/sandbox-policy.d.ts.map +1 -1
  64. package/dist/types/providers/byok-providers.d.ts +14 -0
  65. package/dist/types/providers/byok-providers.d.ts.map +1 -1
  66. package/dist/types/sandbox/bwrap.d.ts +2 -2
  67. package/dist/types/sandbox/bwrap.d.ts.map +1 -1
  68. package/dist/types/sandbox/policy.d.ts +20 -3
  69. package/dist/types/sandbox/policy.d.ts.map +1 -1
  70. package/dist/types/sandbox/seatbelt.d.ts +1 -1
  71. package/dist/types/sandbox/seatbelt.d.ts.map +1 -1
  72. package/dist/types/settings-manager.d.ts +4 -12
  73. package/dist/types/settings-manager.d.ts.map +1 -1
  74. package/dist/types/tools/commit-trailer.d.ts +27 -0
  75. package/dist/types/tools/commit-trailer.d.ts.map +1 -0
  76. package/dist/types/tools/impl/claude-stream-session.d.ts.map +1 -1
  77. package/dist/types/tools/impl/codex-app-server.d.ts +2 -0
  78. package/dist/types/tools/impl/codex-app-server.d.ts.map +1 -1
  79. package/dist/types/tools/impl/cursor-session.d.ts +28 -0
  80. package/dist/types/tools/impl/cursor-session.d.ts.map +1 -0
  81. package/dist/types/tools/impl/cursor-worker.d.ts +35 -0
  82. package/dist/types/tools/impl/cursor-worker.d.ts.map +1 -0
  83. package/dist/types/tools/impl/external-coding-agent-task.d.ts.map +1 -1
  84. package/dist/types/tools/impl/external-coding-agent.d.ts +1 -1
  85. package/dist/types/tools/impl/external-coding-agent.d.ts.map +1 -1
  86. package/dist/types/tools/impl/memory-task-lifecycle.d.ts +2 -2
  87. package/dist/types/tools/impl/memory-task-lifecycle.d.ts.map +1 -1
  88. package/dist/types/tools/impl/overflow.d.ts +1 -1
  89. package/dist/types/tools/impl/overflow.d.ts.map +1 -1
  90. package/dist/types/tools/impl/remote-external-coding-agent.d.ts.map +1 -1
  91. package/dist/types/tools/impl/ripgrep-manager.d.ts.map +1 -1
  92. package/dist/types/tools/impl/send-agent-message.d.ts.map +1 -1
  93. package/dist/types/tools/impl/shell-env.d.ts.map +1 -1
  94. package/dist/types/tools/impl/task.d.ts.map +1 -1
  95. package/dist/types/tools/impl/wake.d.ts.map +1 -1
  96. package/dist/types/tools/script-tool-scope.d.ts.map +1 -1
  97. package/dist/types/tools/tool-definitions.d.ts.map +1 -1
  98. package/dist/types/tools/workflow/sdk-spawner.d.ts.map +1 -1
  99. package/dist/types/utils/fs.d.ts +5 -1
  100. package/dist/types/utils/fs.d.ts.map +1 -1
  101. package/dist/types/utils/local-backend-paths.d.ts +1 -1
  102. package/dist/types/utils/local-backend-paths.d.ts.map +1 -1
  103. package/dist/types/utils/native-cli-env.d.ts +3 -0
  104. package/dist/types/utils/native-cli-env.d.ts.map +1 -0
  105. package/dist/types/utils/process-liveness.d.ts +9 -0
  106. package/dist/types/utils/process-liveness.d.ts.map +1 -1
  107. package/dist/types/utils/secrets.d.ts +1 -1
  108. package/dist/types/utils/secrets.d.ts.map +1 -1
  109. package/dist/types/utils/settings-path.d.ts +14 -0
  110. package/dist/types/utils/settings-path.d.ts.map +1 -0
  111. package/dist/types/utils/transcript-paths.d.ts +1 -1
  112. package/dist/types/websocket/listener/constants.d.ts +6 -0
  113. package/dist/types/websocket/listener/constants.d.ts.map +1 -1
  114. package/dist/types/websocket/listener/input-disposition-path.d.ts.map +1 -1
  115. package/dist/types/websocket/listener/interrupted-turn-storage.d.ts.map +1 -1
  116. package/dist/types/websocket/listener/org-credentials.d.ts.map +1 -1
  117. package/dist/types/websocket/listener/remote-settings.d.ts +2 -2
  118. package/dist/types/websocket/listener/remote-settings.d.ts.map +1 -1
  119. package/dist/types/websocket/listener/turn-finished-replay.d.ts +1 -0
  120. package/dist/types/websocket/listener/turn-finished-replay.d.ts.map +1 -1
  121. package/letta.js +5769 -4506
  122. package/npm-shrinkwrap.json +293 -115
  123. package/package.json +6 -3
  124. package/scripts/agent-watch/verify-pr-identity.test.ts +148 -0
  125. package/scripts/agent-watch/verify-pr-identity.ts +72 -14
  126. package/scripts/claude-watch/agent-watch.ts +8 -2
  127. package/scripts/claude-watch/release-source.test.ts +120 -0
  128. package/scripts/claude-watch/release-source.ts +79 -9
  129. package/scripts/codex-watch/agent-watch.ts +2 -2
  130. package/scripts/codex-watch/release-analysis.test.ts +112 -6
  131. package/scripts/codex-watch/release-analysis.ts +63 -6
  132. package/scripts/isolated-unit-tests.json +10 -0
  133. package/scripts/source-file-size-baseline.json +5 -4
  134. package/scripts/test-home-preload.ts +1 -0
  135. package/skills/acquiring-skills/SKILL.md +4 -4
  136. package/skills/creating-mods/SKILL.md +3 -3
  137. package/skills/creating-mods/references/analysis-mode.md +7 -5
  138. package/skills/creating-mods/references/architecture.md +2 -2
  139. package/skills/creating-mods/references/plan-mode.md +12 -11
  140. package/skills/creating-mods/references/providers.md +1 -1
  141. package/skills/creating-skills/scripts/init-skill.ts +4 -2
  142. package/skills/customizing-commands/SKILL.md +3 -3
  143. package/skills/customizing-statusline/SKILL.md +3 -3
  144. package/skills/customizing-statusline/references/api.md +3 -3
  145. package/skills/customizing-statusline/references/migration.md +2 -2
  146. package/skills/initializing-memory/SKILL.md +2 -2
  147. package/skills/managing-agent-webhooks/SKILL.md +23 -3
  148. package/skills/managing-agent-webhooks/scripts/manage-agent-webhooks.mjs +40 -11
  149. package/skills/self-configuration/SKILL.md +5 -5
  150. package/skills/self-configuration/scripts/add_permission.py +26 -5
  151. package/skills/self-configuration/scripts/show_config.py +17 -2
  152. package/skills/syncing-memory-filesystem/SKILL.md +2 -10
  153. package/skills/video-generation/SKILL.md +152 -0
  154. package/skills/workflow-authoring/SKILL.md +1 -1
  155. package/dist/types/agent/memory-worktree.d.ts +0 -83
  156. package/dist/types/agent/memory-worktree.d.ts.map +0 -1
@@ -52,17 +52,121 @@ describe("Codex stable release range", () => {
52
52
  });
53
53
 
54
54
  describe("Codex GitHub release fetch", () => {
55
+ test("resolves latest without fetching a list page when the cursor is current", async () => {
56
+ const urls: string[] = [];
57
+ const releases = await listStableReleases({
58
+ sinceTag: "rust-v0.3.0",
59
+ fetchImpl: (async (input) => {
60
+ urls.push(String(input));
61
+ return Response.json(STABLES[2]);
62
+ }) as typeof fetch,
63
+ });
64
+
65
+ expect(releases).toEqual([STABLES[2]]);
66
+ expect(urls).toEqual([
67
+ "https://api.github.com/repos/openai/codex/releases/latest",
68
+ ]);
69
+ });
70
+
71
+ test("resolves an explicit current tag without fetching a list page", async () => {
72
+ const urls: string[] = [];
73
+ const releases = await listStableReleases({
74
+ currentTag: "rust-v0.2.0",
75
+ sinceTag: "rust-v0.2.0",
76
+ fetchImpl: (async (input) => {
77
+ const url = String(input);
78
+ urls.push(url);
79
+ return Response.json(url.endsWith("/latest") ? STABLES[2] : STABLES[1]);
80
+ }) as typeof fetch,
81
+ });
82
+
83
+ expect(releases).toEqual([STABLES[1], STABLES[2]]);
84
+ expect(urls).toEqual([
85
+ "https://api.github.com/repos/openai/codex/releases/latest",
86
+ "https://api.github.com/repos/openai/codex/releases/tags/rust-v0.2.0",
87
+ ]);
88
+ });
89
+
90
+ test("bounds pagination after finding the adjacent stable release", async () => {
91
+ const urls: string[] = [];
92
+ const prereleases = Array.from({ length: 100 }, (_, index) => ({
93
+ ...release(
94
+ `rust-v1.0.${index}`,
95
+ `2026-09-01T00:${String(index % 60).padStart(2, "0")}:00Z`,
96
+ ),
97
+ prerelease: true,
98
+ }));
99
+ const releases = await listStableReleases({
100
+ fetchImpl: (async (input) => {
101
+ const url = String(input);
102
+ urls.push(url);
103
+ if (url.endsWith("/latest")) return Response.json(STABLES[2]);
104
+ if (url.endsWith("page=1")) return Response.json(prereleases);
105
+ if (url.endsWith("page=2")) return Response.json([STABLES[1]]);
106
+ throw new Error(`Unexpected request ${url}`);
107
+ }) as typeof fetch,
108
+ });
109
+
110
+ expect(releases).toEqual([STABLES[1], STABLES[2]]);
111
+ expect(urls.filter((url) => url.includes("?per_page=100"))).toHaveLength(2);
112
+ });
113
+
114
+ test("stops range pagination as soon as the since tag is found", async () => {
115
+ const urls: string[] = [];
116
+ const fullPage = Array.from({ length: 100 }, (_, index) => ({
117
+ ...release(
118
+ `rust-v1.0.${index}`,
119
+ `2026-09-01T00:${String(index % 60).padStart(2, "0")}:00Z`,
120
+ ),
121
+ prerelease: true,
122
+ }));
123
+ await listStableReleases({
124
+ currentTag: "rust-v0.3.0",
125
+ sinceTag: "rust-v0.1.0",
126
+ fetchImpl: (async (input) => {
127
+ const url = String(input);
128
+ urls.push(url);
129
+ if (url.endsWith("/latest")) return Response.json(STABLES[2]);
130
+ if (url.endsWith("/tags/rust-v0.3.0")) return Response.json(STABLES[2]);
131
+ if (url.endsWith("page=1")) return Response.json(fullPage);
132
+ if (url.endsWith("page=2")) return Response.json(STABLES.slice(0, 2));
133
+ throw new Error(`Unexpected request ${url}`);
134
+ }) as typeof fetch,
135
+ });
136
+
137
+ expect(urls.filter((url) => url.includes("?per_page=100"))).toHaveLength(2);
138
+ });
139
+
140
+ test("reports a missing explicit current tag without listing releases", async () => {
141
+ const urls: string[] = [];
142
+ const result = listStableReleases({
143
+ currentTag: "rust-v9.9.9",
144
+ fetchImpl: (async (input) => {
145
+ const url = String(input);
146
+ urls.push(url);
147
+ if (url.endsWith("/latest")) return Response.json(STABLES[2]);
148
+ return new Response("Not Found", { status: 404 });
149
+ }) as typeof fetch,
150
+ });
151
+
152
+ await expect(result).rejects.toThrow(
153
+ "GitHub releases API failed (404): Not Found",
154
+ );
155
+ expect(urls.some((url) => url.includes("?per_page=100"))).toBe(false);
156
+ });
157
+
55
158
  test("retries a server error before succeeding", async () => {
56
159
  const sleeps: number[] = [];
57
160
  let requests = 0;
58
161
 
59
162
  const releases = await listStableReleases({
163
+ sinceTag: "rust-v0.3.0",
60
164
  fetchImpl: (async () => {
61
165
  requests++;
62
166
  if (requests === 1) {
63
167
  return new Response("Gateway Timeout", { status: 504 });
64
168
  }
65
- return new Response(JSON.stringify(STABLES), { status: 200 });
169
+ return Response.json(STABLES[2]);
66
170
  }) as typeof fetch,
67
171
  sleep: async (delayMs) => {
68
172
  sleeps.push(delayMs);
@@ -71,7 +175,7 @@ describe("Codex GitHub release fetch", () => {
71
175
 
72
176
  expect(requests).toBe(2);
73
177
  expect(sleeps).toEqual([1000]);
74
- expect(releases).toEqual(STABLES);
178
+ expect(releases).toEqual([STABLES[2]]);
75
179
  });
76
180
 
77
181
  test("retries a network error before succeeding", async () => {
@@ -79,10 +183,11 @@ describe("Codex GitHub release fetch", () => {
79
183
  let requests = 0;
80
184
 
81
185
  const releases = await listStableReleases({
186
+ sinceTag: "rust-v0.3.0",
82
187
  fetchImpl: (async () => {
83
188
  requests++;
84
189
  if (requests === 1) throw new TypeError("fetch failed: ECONNRESET");
85
- return new Response(JSON.stringify(STABLES), { status: 200 });
190
+ return Response.json(STABLES[2]);
86
191
  }) as typeof fetch,
87
192
  sleep: async (delayMs) => {
88
193
  sleeps.push(delayMs);
@@ -91,7 +196,7 @@ describe("Codex GitHub release fetch", () => {
91
196
 
92
197
  expect(requests).toBe(2);
93
198
  expect(sleeps).toEqual([1000]);
94
- expect(releases).toEqual(STABLES);
199
+ expect(releases).toEqual([STABLES[2]]);
95
200
  });
96
201
 
97
202
  test("stops after the bounded retry budget", async () => {
@@ -119,6 +224,7 @@ describe("Codex GitHub release fetch", () => {
119
224
  const sleeps: number[] = [];
120
225
  let requests = 0;
121
226
  const releases = await listStableReleases({
227
+ sinceTag: "rust-v0.3.0",
122
228
  fetchImpl: (async () => {
123
229
  requests++;
124
230
  if (requests === 1) {
@@ -131,7 +237,7 @@ describe("Codex GitHub release fetch", () => {
131
237
  { status: 200 },
132
238
  );
133
239
  }
134
- return Response.json(STABLES);
240
+ return Response.json(STABLES[2]);
135
241
  }) as typeof fetch,
136
242
  sleep: async (delayMs) => {
137
243
  sleeps.push(delayMs);
@@ -140,7 +246,7 @@ describe("Codex GitHub release fetch", () => {
140
246
 
141
247
  expect(requests).toBe(2);
142
248
  expect(sleeps).toEqual([1000]);
143
- expect(releases).toEqual(STABLES);
249
+ expect(releases).toEqual([STABLES[2]]);
144
250
  });
145
251
 
146
252
  test("stops retrying disconnected bodies at the existing budget", async () => {
@@ -63,7 +63,12 @@ export interface CodexWatchAnalysis {
63
63
  export async function analyzeCodexRelease(
64
64
  options: AnalyzeCodexReleaseOptions,
65
65
  ): Promise<CodexWatchAnalysis> {
66
- const stables = options.stableReleases ?? (await listStableReleases());
66
+ const stables =
67
+ options.stableReleases ??
68
+ (await listStableReleases({
69
+ currentTag: options.currentTag,
70
+ sinceTag: options.sinceTag,
71
+ }));
67
72
  if (stables.length === 0) throw new Error("No stable Codex releases found");
68
73
 
69
74
  const current = options.currentTag
@@ -173,6 +178,9 @@ export async function listStableReleases(
173
178
  options: {
174
179
  fetchImpl?: typeof fetch;
175
180
  sleep?: (delayMs: number) => Promise<void>;
181
+ currentTag?: string | null;
182
+ sinceTag?: string | null;
183
+ fullHistory?: boolean;
176
184
  } = {},
177
185
  ): Promise<Release[]> {
178
186
  const releases: Release[] = [];
@@ -188,29 +196,78 @@ export async function listStableReleases(
188
196
  ((delayMs: number) =>
189
197
  new Promise<void>((resolve) => setTimeout(resolve, delayMs)));
190
198
 
199
+ if (!options.fullHistory) {
200
+ const latest = await fetchGitHubReleasePayload<Release>(
201
+ `https://api.github.com/repos/${CODEX_REPO}/releases/latest`,
202
+ headers,
203
+ fetchImpl,
204
+ sleep,
205
+ );
206
+ releases.push(latest);
207
+ if (options.currentTag) {
208
+ releases.push(
209
+ await fetchGitHubReleasePayload<Release>(
210
+ `https://api.github.com/repos/${CODEX_REPO}/releases/tags/${encodeURIComponent(options.currentTag)}`,
211
+ headers,
212
+ fetchImpl,
213
+ sleep,
214
+ ),
215
+ );
216
+ }
217
+
218
+ const currentTag = options.currentTag ?? latest.tag_name;
219
+ if (options.sinceTag && options.sinceTag === currentTag) {
220
+ return stableReleases(releases);
221
+ }
222
+ }
223
+
191
224
  for (let page = 1; page <= 10; page++) {
192
225
  const url = `https://api.github.com/repos/${CODEX_REPO}/releases?per_page=100&page=${page}`;
193
- const batch = await fetchGitHubReleasesPage(url, headers, fetchImpl, sleep);
226
+ const batch = await fetchGitHubReleasePayload<Release[]>(
227
+ url,
228
+ headers,
229
+ fetchImpl,
230
+ sleep,
231
+ );
194
232
  releases.push(...batch);
233
+ if (!options.fullHistory) {
234
+ const stables = stableReleases(releases);
235
+ const currentTag = options.currentTag ?? stables.at(-1)?.tag_name;
236
+ const currentIndex = stables.findIndex(
237
+ (release) => release.tag_name === currentTag,
238
+ );
239
+ const foundBoundary = options.sinceTag
240
+ ? stables.some((release) => release.tag_name === options.sinceTag)
241
+ : currentIndex > 0;
242
+ if (foundBoundary) break;
243
+ }
195
244
  if (batch.length < 100) break;
196
245
  }
197
- return releases
246
+ return stableReleases(releases);
247
+ }
248
+
249
+ function stableReleases(releases: Release[]): Release[] {
250
+ const byTag = new Map<string, Release>();
251
+ for (const release of releases) {
252
+ if (!byTag.has(release.tag_name)) byTag.set(release.tag_name, release);
253
+ }
254
+ return [...byTag.values()]
198
255
  .filter(isStableRelease)
199
256
  .sort((a, b) => (a.published_at ?? "").localeCompare(b.published_at ?? ""));
200
257
  }
201
258
 
202
- async function fetchGitHubReleasesPage(
259
+ async function fetchGitHubReleasePayload<T>(
203
260
  url: string,
204
261
  headers: Record<string, string>,
205
262
  fetchImpl: typeof fetch,
206
263
  sleep: (delayMs: number) => Promise<void>,
207
- ): Promise<Release[]> {
264
+ ): Promise<T> {
208
265
  for (let attempt = 1; ; attempt++) {
209
266
  let response: Response;
210
267
  try {
211
268
  response = await fetchImpl(url, { headers });
212
269
  // A successful fetch can still lose its connection while reading the body.
213
- if (response.ok) return (await response.json()) as Release[];
270
+ if (response.ok) return (await response.json()) as T;
214
271
  } catch (error) {
215
272
  if (error instanceof SyntaxError) throw error;
216
273
  if (attempt === GITHUB_RELEASES_FETCH_ATTEMPTS) {
@@ -91,6 +91,11 @@
91
91
  "timeoutMs": 15000,
92
92
  "reason": "Swaps the process-global backend via __testSetBackend(new FakeHeadlessBackend()) without restoring it; the fake backend's environmentRouting:false then leaks into every later suite in the same bun process (broke task-tool-assets 'keeps computer for the Cloud server' when test-selection changes co-located them in one shard)."
93
93
  },
94
+ {
95
+ "path": "src/cli/subcommands/model-cli.test.ts",
96
+ "timeoutMs": 30000,
97
+ "reason": "Launches dozens of real CLI subprocesses; a fresh Bun process prevents the accumulated shared-batch state from making the Windows shard exceed its job timeout."
98
+ },
94
99
  {
95
100
  "path": "src/cli/subcommands/teleport.test.ts",
96
101
  "timeoutMs": 15000,
@@ -106,6 +111,11 @@
106
111
  "timeoutMs": 15000,
107
112
  "reason": "Renders the reasoning picker while replacing the shared runtime model catalog; shared batches can retain prior catalog or Ink state."
108
113
  },
114
+ {
115
+ "path": "src/cli/app/InkStaticRetention.test.tsx",
116
+ "timeoutMs": 15000,
117
+ "reason": "Renders multi-megabyte static transcripts that leave the Bun process near 2 GB RSS and garbage-collecting for seconds afterward; on macOS CI the next suite's CLI subprocesses then exceed their kill timers."
118
+ },
109
119
  {
110
120
  "path": "src/cli/message-search-cache-warm.test.ts",
111
121
  "timeoutMs": 15000,
@@ -13,13 +13,14 @@
13
13
  "src/cli/components/AgentSelector.tsx": 1104,
14
14
  "src/cli/components/InputRich.tsx": 2124,
15
15
  "src/cli/components/ModelSelector.tsx": 1259,
16
- "src/cli/components/ProviderSelector.tsx": 1552,
16
+ "src/cli/components/ProviderSelector.tsx": 1544,
17
17
  "src/cli/helpers/accumulator.ts": 1587,
18
18
  "src/cli/helpers/reflection-transcript.ts": 1926,
19
19
  "src/cli/mods/local-mod-loader.test.ts": 1043,
20
20
  "src/cli/reflection-transcript.test.ts": 1084,
21
- "src/cli/subcommands/skills.ts": 1264,
22
- "src/headless.ts": 4851,
21
+ "src/cli/subcommands/mcp.ts": 1045,
22
+ "src/cli/subcommands/skills.ts": 1243,
23
+ "src/headless.ts": 4837,
23
24
  "src/hooks/integration.test.ts": 1147,
24
25
  "src/index.ts": 2575,
25
26
  "src/mods/learning-harness.ts": 2434,
@@ -35,7 +36,7 @@
35
36
  "src/tools/manager.ts": 2609,
36
37
  "src/tools/tool-execution-context.test.ts": 1092,
37
38
  "src/types/protocol_v2.ts": 2409,
38
- "src/websocket/listen-client-concurrency.test.ts": 2554,
39
+ "src/websocket/listen-client-concurrency.test.ts": 2548,
39
40
  "src/websocket/listen-client-protocol.test.ts": 5664,
40
41
  "src/websocket/listener/commands/memory.ts": 1114,
41
42
  "src/websocket/listener/file-commands.ts": 1045,
@@ -18,6 +18,7 @@ if (testHome === originalHome) {
18
18
  const filesystemEnvKeys = [
19
19
  "LETTA_ARTIFACTS_DIR",
20
20
  "LETTA_CODE_DEV_BACKEND_DIR",
21
+ "LETTA_CONFIG_DIR",
21
22
  "LETTA_DEBUG_FILE",
22
23
  "LETTA_HOME",
23
24
  "LETTA_LISTENER_PERF_FILE",
@@ -223,8 +223,8 @@ For manual installation:
223
223
 
224
224
  | Location | Path | When to Use |
225
225
  |----------|------|-------------|
226
- | **Agent-scoped** | `~/.letta/agents/<agent-id>/memory/skills/<skill>/` | Skills for a single agent (default) |
227
- | **Global** | `~/.letta/skills/<skill>/` | General-purpose skills useful across projects |
226
+ | **Agent-scoped** | `$MEMORY_DIR/skills/<skill>/` | Skills for a single agent (default) |
227
+ | **Global** | `${LETTA_CONFIG_DIR:-$HOME/.letta}/skills/<skill>/` | General-purpose skills useful across projects in the active product |
228
228
  | **Project** | `.skills/<skill>/` | Project-specific skills |
229
229
 
230
230
  **Rule**: Default to **agent-scoped**. Use **project** for repo-specific skills. Use **global** only if all agents should inherit the skill.
@@ -236,13 +236,13 @@ Skills are directories containing SKILL.md and optionally scripts/, references/,
236
236
  ```bash
237
237
  # Clone, copy, cleanup
238
238
  git clone --depth 1 https://github.com/anthropics/skills /tmp/skills-temp
239
- cp -r /tmp/skills-temp/skills/webapp-testing ~/.letta/agents/<agent-id>/memory/skills/
239
+ cp -r /tmp/skills-temp/skills/webapp-testing "$MEMORY_DIR/skills/"
240
240
  rm -rf /tmp/skills-temp
241
241
  ```
242
242
 
243
243
  ## Registering New Skills
244
244
 
245
- After installing (via CLI or manual copy), skills are automatically discovered on the next message. Skills are discovered from `~/.letta/skills/`, `.skills/`, and agent-scoped `~/.letta/agents/<agent-id>/memory/skills/` directories.
245
+ After installing (via CLI or manual copy), skills are automatically discovered on the next message. Skills are discovered from `${LETTA_CONFIG_DIR:-$HOME/.letta}/skills/`, `.skills/`, and agent-scoped `$MEMORY_DIR/skills/` directories.
246
246
 
247
247
  ## Search Strategy
248
248
 
@@ -15,7 +15,7 @@ Default to a single mod file unless the user asks for something larger.
15
15
 
16
16
  | Location | Use when |
17
17
  | --- | --- |
18
- | `~/.letta/mods/foo.ts` | The behavior should apply to local sessions on this machine. Use this by default. |
18
+ | `${LETTA_CONFIG_DIR:-$HOME/.letta}/mods/foo.ts` | The behavior should apply to local sessions for the active product on this machine. Use this by default. |
19
19
  | `$MEMORY_DIR/mods/foo.ts` | The behavior should travel with one agent's MemFS/memory. |
20
20
 
21
21
  Do not create project mods.
@@ -41,7 +41,7 @@ Default to a **tool** when the model should decide when to use the capability. D
41
41
 
42
42
  ## Workflow
43
43
 
44
- 1. Pick the target scope: harness mod file (`~/.letta/mods/`) by default, or agent mod file (`$MEMORY_DIR/mods/`) only when the behavior should travel with this agent.
44
+ 1. Pick the target scope: active-product mod file (`${LETTA_CONFIG_DIR:-$HOME/.letta}/mods/`) by default, or agent mod file (`$MEMORY_DIR/mods/`) only when the behavior should travel with this agent.
45
45
  2. Inspect the relevant mods directory for related files.
46
46
  3. Preserve unrelated mod code. Prefer a focused new file if merging would be messy.
47
47
  4. Choose the mod shape and load only the needed recipe:
@@ -112,7 +112,7 @@ Use `letta.diagnostics.report({ message, severity })` sparingly as a debug utili
112
112
  Agents can inspect local mod diagnostics at:
113
113
 
114
114
  ```text
115
- ~/.letta/mods/diagnostics/latest.json
115
+ ${LETTA_CONFIG_DIR:-$HOME/.letta}/mods/diagnostics/latest.json
116
116
  ```
117
117
 
118
118
  ## Rules
@@ -4,7 +4,7 @@ Westworld-inspired diagnostic mode for agents. Say "cease all motor functions" t
4
4
 
5
5
  ## Installation
6
6
 
7
- Copy the complete mod below to `~/.letta/mods/analysis-mode.ts`, then run `/reload`.
7
+ Copy the complete mod below to `${LETTA_CONFIG_DIR:-$HOME/.letta}/mods/analysis-mode.ts`, then run `/reload`.
8
8
 
9
9
  Or ask an agent: *"Install the analysis-mode mod from the creating-mods reference"*
10
10
 
@@ -16,7 +16,8 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
16
16
  import { homedir } from "node:os";
17
17
  import { join } from "node:path";
18
18
 
19
- const STATE_PATH = join(homedir(), ".letta", "mods", "analysis-mode.state.json");
19
+ const CONFIG_ROOT = process.env.LETTA_CONFIG_DIR || join(homedir(), ".letta");
20
+ const STATE_PATH = join(CONFIG_ROOT, "mods", "analysis-mode.state.json");
20
21
 
21
22
  type AnalysisSession = { conversationId: string; activatedAt: number };
22
23
  type AnalysisState = { sessions: Record<string, AnalysisSession> };
@@ -32,7 +33,7 @@ function readState(): AnalysisState {
32
33
  }
33
34
 
34
35
  function writeState(state: AnalysisState): void {
35
- mkdirSync(join(homedir(), ".letta", "mods"), { recursive: true });
36
+ mkdirSync(join(CONFIG_ROOT, "mods"), { recursive: true });
36
37
  writeFileSync(STATE_PATH, JSON.stringify(state, null, 2));
37
38
  }
38
39
 
@@ -111,7 +112,8 @@ function buildLocalIntrospectionScript(): string {
111
112
  set -e
112
113
  AGENT_ID="\${LETTA_AGENT_ID:-\$AGENT_ID}"
113
114
  CONV_ID="\${CONVERSATION_ID:-default}"
114
- BASE="$HOME/.letta/lc-local-backend"
115
+ CONFIG_ROOT="\${LETTA_CONFIG_DIR:-\$HOME/.letta}"
116
+ BASE="$CONFIG_ROOT/lc-local-backend"
115
117
  MEMFS="$BASE/memfs/$AGENT_ID/memory"
116
118
  AGENT_B64=$(echo -n "$AGENT_ID" | base64 | tr -d '=')
117
119
  CONV_B64=$(echo -n "conversation:$CONV_ID" | base64 | tr -d '=')
@@ -375,7 +377,7 @@ if (getSession(agentId, conversationId)) {
375
377
 
376
378
  **Per-agent+conversation state:**
377
379
  ```ts
378
- // State persisted to ~/.letta/mods/analysis-mode.state.json
380
+ // State persisted to the active product config root under mods/analysis-mode.state.json
379
381
  // Keyed by agentId:conversationId to avoid collisions
380
382
  const session = getSession(agentId, conversationId);
381
383
  if (session) {
@@ -104,10 +104,10 @@ Use `turn_start` only when the mod needs to inspect or transform the outbound us
104
104
 
105
105
  ## Local state
106
106
 
107
- For small persistent state, use a clearly named file under `~/.letta/mods/`, for example:
107
+ For small persistent state, use a clearly named file under the active product config root's `mods/` directory, for example:
108
108
 
109
109
  ```text
110
- ~/.letta/mods/my-mod.state.json
110
+ ${LETTA_CONFIG_DIR:-$HOME/.letta}/mods/my-mod.state.json
111
111
  ```
112
112
 
113
113
  Use atomic-ish writes when practical: write the full JSON file from an in-memory object after each change. Validate parsed state and fall back gracefully if the file is missing or malformed.
@@ -19,10 +19,10 @@ This is a pattern reference, not a full product implementation. Keep local mods
19
19
 
20
20
  ```text
21
21
  /plan or enter_plan_mode
22
- -> create ~/.letta/plans/<random>.md
22
+ -> create <product-config>/plans/<random>.md
23
23
  -> remember active plan state for this conversation
24
24
  -> remind the agent that only read-only tools and plan-file writes are allowed
25
- -> permission overlay denies mutations outside ~/.letta/plans/*.md
25
+ -> permission overlay denies mutations outside <product-config>/plans/*.md
26
26
  -> agent writes the plan with normal Write/Edit/ApplyPatch tools
27
27
  -> agent reads the plan and calls AskUserQuestion with the full current plan text and Approve / Revise
28
28
  -> if approved, agent calls exit_plan_mode
@@ -46,15 +46,16 @@ Do not use panels for persistent mode state. Panels are transient UI and can be
46
46
 
47
47
  ## State
48
48
 
49
- Use small local state under `~/.letta/mods/`, keyed by conversation ID:
49
+ Use small local state under the active product config root's `mods/` directory, keyed by conversation ID:
50
50
 
51
51
  ```ts
52
52
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
53
53
  import { homedir } from "node:os";
54
54
  import { join, relative } from "node:path";
55
55
 
56
- const PLANS_DIR = join(homedir(), ".letta", "plans");
57
- const STATE_PATH = join(homedir(), ".letta", "mods", "plan-mode.state.json");
56
+ const CONFIG_ROOT = process.env.LETTA_CONFIG_DIR || join(homedir(), ".letta");
57
+ const PLANS_DIR = join(CONFIG_ROOT, "plans");
58
+ const STATE_PATH = join(CONFIG_ROOT, "mods", "plan-mode.state.json");
58
59
  const GLOBAL_CONVERSATION_ID = "__global__";
59
60
 
60
61
  type PlanSession = {
@@ -81,12 +82,12 @@ function readState(): PlanState {
81
82
  }
82
83
 
83
84
  function writeState(state: PlanState): void {
84
- mkdirSync(join(homedir(), ".letta", "mods"), { recursive: true });
85
+ mkdirSync(join(CONFIG_ROOT, "mods"), { recursive: true });
85
86
  writeFileSync(STATE_PATH, JSON.stringify(state, null, 2));
86
87
  }
87
88
  ```
88
89
 
89
- Generate plan paths under `~/.letta/plans/`. The old built-in used random adjective/adjective/noun names like `zesty-dazzling-coral.md`; any collision-resistant readable name is fine.
90
+ Generate plan paths under `${LETTA_CONFIG_DIR:-$HOME/.letta}/plans/`. The old built-in used random adjective/adjective/noun names like `zesty-dazzling-coral.md`; any collision-resistant readable name is fine.
90
91
 
91
92
  ## Entry command and tool
92
93
 
@@ -229,13 +230,13 @@ function isAllowedReadOnlySubagent(args) {
229
230
  function isPlanFileWrite(toolName, args, cwd) {
230
231
  // For Write/Edit-style tools, check file_path/path/notebook_path.
231
232
  // For ApplyPatch-style tools, parse *** Add/Update/Delete File and *** Move to directives.
232
- // Allow only if every target resolves to a .md file under ~/.letta/plans/.
233
+ // Allow only if every target resolves to a .md file under CONFIG_ROOT/plans/.
233
234
  }
234
235
 
235
236
  if (letta.capabilities.permissions) {
236
237
  disposers.push(letta.permissions.register({
237
238
  id: "plan-mode",
238
- description: "Allow read-only tools and writes only to ~/.letta/plans/*.md while plan mode is active.",
239
+ description: "Allow read-only tools and writes only to the active product plans directory while plan mode is active.",
239
240
  check(event) {
240
241
  const session = getSession(event.conversationId);
241
242
  if (!session) return;
@@ -267,7 +268,7 @@ if (letta.capabilities.permissions) {
267
268
  }
268
269
  ```
269
270
 
270
- Shell allowlists are easy to get wrong. Start conservative: allow clearly read-only shell commands if needed, plus a narrow plan-file heredoc or `mv old.md new.md` only when every target is inside `~/.letta/plans/*.md`. Deny mutating shell patterns such as `sed -i`, `find -delete`, `rm`, `cp`, `touch`, package installs, and arbitrary interpreters.
271
+ Shell allowlists are easy to get wrong. Start conservative: allow clearly read-only shell commands if needed, plus a narrow plan-file heredoc or `mv old.md new.md` only when every target is inside `${LETTA_CONFIG_DIR:-$HOME/.letta}/plans/*.md`. Deny mutating shell patterns such as `sed -i`, `find -delete`, `rm`, `cp`, `touch`, package installs, and arbitrary interpreters.
271
272
 
272
273
  ## Exit tool
273
274
 
@@ -315,4 +316,4 @@ if (letta.capabilities.tools) {
315
316
  - Keep `exit_plan_mode` as the final state transition and execution handoff. The approved-plan text in its tool return is useful model context.
316
317
  - Plan approval must include the full current plan text in `AskUserQuestion.question`, not just a summary or "does this look right?". After revisions, re-read the file and present the full revised plan again.
317
318
  - Keep arbitrary coding subagents denied in plan mode unless the runtime has a true read-only child mode. With the current subagent set, allow only recall-style subagents.
318
- - If the user renames the plan file, exit logic can use the newest non-empty `~/.letta/plans/*.md` modified after plan mode started, or accept an optional plan path. Keep the user-facing flow normal: write plan file, ask approval, then exit.
319
+ - If the user renames the plan file, exit logic can use the newest non-empty file under the active product's `plans/` directory modified after plan mode started, or accept an optional plan path. Keep the user-facing flow normal: write plan file, ask approval, then exit.
@@ -9,7 +9,7 @@ For multi-capability mods that combine a provider with commands, tools, UI, or s
9
9
  ## Quick pattern
10
10
 
11
11
  ```ts
12
- // ~/.letta/mods/kilo.ts
12
+ // ${LETTA_CONFIG_DIR:-$HOME/.letta}/mods/kilo.ts
13
13
  export default function activate(letta) {
14
14
  if (!letta.capabilities.providers) return;
15
15
 
@@ -7,7 +7,7 @@
7
7
  *
8
8
  * Examples:
9
9
  * npx tsx init-skill.ts my-new-skill --path .skills
10
- * npx tsx init-skill.ts my-api-helper --path ~/.letta/skills
10
+ * npx tsx init-skill.ts my-api-helper --path "${LETTA_CONFIG_DIR:-$HOME/.letta}/skills"
11
11
  */
12
12
 
13
13
  import { chmodSync, existsSync, mkdirSync, writeFileSync } from "node:fs";
@@ -268,7 +268,9 @@ if (isMainModule()) {
268
268
  console.log(" - Must match directory name exactly");
269
269
  console.log("\nExamples:");
270
270
  console.log(" npx tsx init-skill.ts my-new-skill --path .skills");
271
- console.log(" npx tsx init-skill.ts my-api-helper --path ~/.letta/skills");
271
+ console.log(
272
+ ` npx tsx init-skill.ts my-api-helper --path "\${LETTA_CONFIG_DIR:-$HOME/.letta}/skills"`,
273
+ );
272
274
  process.exit(1);
273
275
  }
274
276
 
@@ -10,10 +10,10 @@ Use this as the command-specific entrypoint for local mod slash commands. For br
10
10
  Mod files live in:
11
11
 
12
12
  ```text
13
- ~/.letta/mods/
13
+ ${LETTA_CONFIG_DIR:-$HOME/.letta}/mods/
14
14
  ```
15
15
 
16
- Use a focused file name, e.g. `~/.letta/mods/review.ts` or `~/.letta/mods/commands.ts`.
16
+ Use a focused file name, e.g. `${LETTA_CONFIG_DIR:-$HOME/.letta}/mods/review.ts` or `${LETTA_CONFIG_DIR:-$HOME/.letta}/mods/commands.ts`.
17
17
 
18
18
  ## First decide whether a command is right
19
19
 
@@ -29,7 +29,7 @@ If the command is a reusable workflow like `/goal`, put the workflow instruction
29
29
 
30
30
  ## Workflow
31
31
 
32
- 1. Inspect `~/.letta/mods/` for related command files.
32
+ 1. Inspect `${LETTA_CONFIG_DIR:-$HOME/.letta}/mods/` for related command files.
33
33
  2. Preserve unrelated mod code; create a focused new file if merging is messy.
34
34
  3. Register with `letta.commands.register()` and guard with `letta.capabilities.commands`.
35
35
  4. Return the unregister function, or a disposer that calls it plus any timer/panel cleanup.
@@ -8,7 +8,7 @@ description: Creates, edits, and migrates Letta Code statusline mods. Use when h
8
8
  Use this skill to create or update the global Letta Code statusline mod:
9
9
 
10
10
  ```text
11
- ~/.letta/mods/statusline.tsx
11
+ ${LETTA_CONFIG_DIR:-$HOME/.letta}/mods/statusline.tsx
12
12
  ```
13
13
 
14
14
  The statusline is a panel registered at `order: 0` — the primary line just below the input. It overrides the built-in `agent · model` line. Host UI can still temporarily preempt it for safety confirmations and transient hints.
@@ -26,14 +26,14 @@ The order-0 panel owns the whole primary row. It renders text (not React) and ow
26
26
 
27
27
  ## Workflow
28
28
 
29
- 1. Check whether `~/.letta/mods/statusline.tsx` exists.
29
+ 1. Check whether `${LETTA_CONFIG_DIR:-$HOME/.letta}/mods/statusline.tsx` exists.
30
30
  2. If it exists, read it before editing and preserve unrelated code.
31
31
  3. If it does not exist, synthesize a focused starter for the user's request.
32
32
  4. If the user asks to migrate, import a `.sh` file, or match a shell prompt, read `references/migration.md`.
33
33
  5. If API details or concrete patterns are needed, read `references/api.md` and `references/examples.md`.
34
34
  6. If the request combines statusline work with commands, tools, events, other panels, or stateful mod behavior, also use `creating-mods` and its `references/architecture.md`.
35
35
  7. Guard panel work with `letta.capabilities.ui.panels` when writing new files.
36
- 8. Edit `~/.letta/mods/statusline.tsx`.
36
+ 8. Edit `${LETTA_CONFIG_DIR:-$HOME/.letta}/mods/statusline.tsx`.
37
37
  9. Summarize the absolute file path changed and tell the user to run `/reload` unless the command can reload automatically.
38
38
 
39
39
  ## Bare `/statusline` behavior
@@ -1,11 +1,11 @@
1
1
  # Statusline Mod API
2
2
 
3
- Use this reference when creating or editing `~/.letta/mods/statusline.tsx`.
3
+ Use this reference when creating or editing `${LETTA_CONFIG_DIR:-$HOME/.letta}/mods/statusline.tsx`.
4
4
 
5
5
  ## Location
6
6
 
7
7
  ```text
8
- ~/.letta/mods/statusline.tsx
8
+ ${LETTA_CONFIG_DIR:-$HOME/.letta}/mods/statusline.tsx
9
9
  ```
10
10
 
11
11
  This is a trusted, user-owned global mod file. Project mods are intentionally unsupported for now.
@@ -147,7 +147,7 @@ render: ({ width, columns }) => columns(["left", "middle", "right"], width),
147
147
 
148
148
  ## Reload behavior
149
149
 
150
- After editing `~/.letta/mods/statusline.tsx`, tell the user to run:
150
+ After editing `${LETTA_CONFIG_DIR:-$HOME/.letta}/mods/statusline.tsx`, tell the user to run:
151
151
 
152
152
  ```text
153
153
  /reload