@letta-ai/letta-code 0.34.24 → 0.34.26

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 (81) hide show
  1. package/cursor-worker.js +167 -0
  2. package/dist/agent-presets.js +1 -64
  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 +233 -75
  9. package/dist/mcp-oauth.js.map +9 -9
  10. package/dist/types/agent/subagents/index.d.ts +4 -0
  11. package/dist/types/agent/subagents/index.d.ts.map +1 -1
  12. package/dist/types/agent/subagents/manager.d.ts.map +1 -1
  13. package/dist/types/agent/subagents/memory-worker.d.ts +2 -8
  14. package/dist/types/agent/subagents/memory-worker.d.ts.map +1 -1
  15. package/dist/types/backend/api/providers.d.ts +8 -2
  16. package/dist/types/backend/api/providers.d.ts.map +1 -1
  17. package/dist/types/backend/api/schedules.d.ts +2 -2
  18. package/dist/types/backend/api/schedules.d.ts.map +1 -1
  19. package/dist/types/backend/backend.d.ts +5 -2
  20. package/dist/types/backend/backend.d.ts.map +1 -1
  21. package/dist/types/backend/dev/fake-headless-backend.d.ts +2 -1
  22. package/dist/types/backend/dev/fake-headless-backend.d.ts.map +1 -1
  23. package/dist/types/backend/dev/pi-model-factory.d.ts.map +1 -1
  24. package/dist/types/backend/local/local-provider-auth-store.d.ts +2 -0
  25. package/dist/types/backend/local/local-provider-auth-store.d.ts.map +1 -1
  26. package/dist/types/backend/local/local-transcript-history.d.ts +10 -0
  27. package/dist/types/backend/local/local-transcript-history.d.ts.map +1 -0
  28. package/dist/types/backend/local/local-transcript.d.ts +6 -0
  29. package/dist/types/backend/local/local-transcript.d.ts.map +1 -1
  30. package/dist/types/channels/slack/attachment-primitives.d.ts +2 -1
  31. package/dist/types/channels/slack/attachment-primitives.d.ts.map +1 -1
  32. package/dist/types/cli/subcommands/mcp.d.ts +2 -2
  33. package/dist/types/cli/subcommands/mcp.d.ts.map +1 -1
  34. package/dist/types/cron/process-identity.d.ts.map +1 -1
  35. package/dist/types/cron/run-log.d.ts +1 -0
  36. package/dist/types/cron/run-log.d.ts.map +1 -1
  37. package/dist/types/headless-environment-response.d.ts +27 -0
  38. package/dist/types/headless-environment-response.d.ts.map +1 -1
  39. package/dist/types/providers/byok-providers.d.ts +14 -0
  40. package/dist/types/providers/byok-providers.d.ts.map +1 -1
  41. package/dist/types/tools/impl/claude-stream-session.d.ts.map +1 -1
  42. package/dist/types/tools/impl/codex-app-server.d.ts.map +1 -1
  43. package/dist/types/tools/impl/cursor-session.d.ts +26 -0
  44. package/dist/types/tools/impl/cursor-session.d.ts.map +1 -0
  45. package/dist/types/tools/impl/cursor-worker.d.ts +32 -0
  46. package/dist/types/tools/impl/cursor-worker.d.ts.map +1 -0
  47. package/dist/types/tools/impl/external-coding-agent-task.d.ts.map +1 -1
  48. package/dist/types/tools/impl/external-coding-agent.d.ts +1 -1
  49. package/dist/types/tools/impl/external-coding-agent.d.ts.map +1 -1
  50. package/dist/types/tools/impl/memory-task-lifecycle.d.ts +2 -2
  51. package/dist/types/tools/impl/memory-task-lifecycle.d.ts.map +1 -1
  52. package/dist/types/tools/impl/remote-external-coding-agent.d.ts.map +1 -1
  53. package/dist/types/tools/impl/send-agent-message.d.ts.map +1 -1
  54. package/dist/types/tools/impl/task.d.ts.map +1 -1
  55. package/dist/types/tools/impl/wake.d.ts +4 -2
  56. package/dist/types/tools/impl/wake.d.ts.map +1 -1
  57. package/dist/types/tools/workflow/sdk-spawner.d.ts.map +1 -1
  58. package/dist/types/utils/native-cli-env.d.ts +3 -0
  59. package/dist/types/utils/native-cli-env.d.ts.map +1 -0
  60. package/dist/types/utils/process-liveness.d.ts +9 -0
  61. package/dist/types/utils/process-liveness.d.ts.map +1 -1
  62. package/letta.js +3553 -2911
  63. package/npm-shrinkwrap.json +291 -117
  64. package/package.json +5 -3
  65. package/scripts/agent-watch/verify-pr-identity.test.ts +148 -0
  66. package/scripts/agent-watch/verify-pr-identity.ts +72 -14
  67. package/scripts/claude-watch/agent-watch.ts +8 -2
  68. package/scripts/claude-watch/release-source.test.ts +120 -0
  69. package/scripts/claude-watch/release-source.ts +79 -9
  70. package/scripts/codex-watch/agent-watch.ts +2 -2
  71. package/scripts/codex-watch/release-analysis.test.ts +180 -4
  72. package/scripts/codex-watch/release-analysis.ts +65 -8
  73. package/scripts/source-file-size-baseline.json +5 -4
  74. package/skills/initializing-memory/SKILL.md +1 -1
  75. package/skills/maintaining-deferred-memory/SKILL.md +2 -0
  76. package/skills/managing-agent-webhooks/SKILL.md +23 -3
  77. package/skills/managing-agent-webhooks/scripts/manage-agent-webhooks.mjs +40 -11
  78. package/skills/self-configuration/SKILL.md +1 -1
  79. package/skills/syncing-memory-filesystem/SKILL.md +2 -10
  80. package/dist/types/agent/memory-worktree.d.ts +0 -83
  81. 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 () => {
@@ -115,6 +220,77 @@ describe("Codex GitHub release fetch", () => {
115
220
  expect(sleeps).toEqual([1000, 2000]);
116
221
  });
117
222
 
223
+ test("retries a disconnected successful response body", async () => {
224
+ const sleeps: number[] = [];
225
+ let requests = 0;
226
+ const releases = await listStableReleases({
227
+ sinceTag: "rust-v0.3.0",
228
+ fetchImpl: (async () => {
229
+ requests++;
230
+ if (requests === 1) {
231
+ return new Response(
232
+ new ReadableStream({
233
+ pull(controller) {
234
+ controller.error(new TypeError("fetch failed: ECONNRESET"));
235
+ },
236
+ }),
237
+ { status: 200 },
238
+ );
239
+ }
240
+ return Response.json(STABLES[2]);
241
+ }) as typeof fetch,
242
+ sleep: async (delayMs) => {
243
+ sleeps.push(delayMs);
244
+ },
245
+ });
246
+
247
+ expect(requests).toBe(2);
248
+ expect(sleeps).toEqual([1000]);
249
+ expect(releases).toEqual([STABLES[2]]);
250
+ });
251
+
252
+ test("stops retrying disconnected bodies at the existing budget", async () => {
253
+ const sleeps: number[] = [];
254
+ let requests = 0;
255
+ const result = listStableReleases({
256
+ fetchImpl: (async () => {
257
+ requests++;
258
+ return new Response(
259
+ new ReadableStream({
260
+ pull(controller) {
261
+ controller.error(new TypeError("fetch failed: ECONNRESET"));
262
+ },
263
+ }),
264
+ );
265
+ }) as typeof fetch,
266
+ sleep: async (delayMs) => {
267
+ sleeps.push(delayMs);
268
+ },
269
+ });
270
+
271
+ await expect(result).rejects.toThrow("after 3 attempts");
272
+ expect(requests).toBe(3);
273
+ expect(sleeps).toEqual([1000, 2000]);
274
+ });
275
+
276
+ test("does not retry malformed JSON", async () => {
277
+ const sleeps: number[] = [];
278
+ let requests = 0;
279
+ const result = listStableReleases({
280
+ fetchImpl: (async () => {
281
+ requests++;
282
+ return new Response("not JSON");
283
+ }) as typeof fetch,
284
+ sleep: async (delayMs) => {
285
+ sleeps.push(delayMs);
286
+ },
287
+ });
288
+
289
+ await expect(result).rejects.toBeInstanceOf(SyntaxError);
290
+ expect(requests).toBe(1);
291
+ expect(sleeps).toEqual([]);
292
+ });
293
+
118
294
  test("fails immediately on a client error", async () => {
119
295
  const sleeps: number[] = [];
120
296
  let requests = 0;
@@ -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,80 @@ 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 res = await fetchGitHubReleasesPage(url, headers, fetchImpl, sleep);
194
- const batch = (await res.json()) as Release[];
226
+ const batch = await fetchGitHubReleasePayload<Release[]>(
227
+ url,
228
+ headers,
229
+ fetchImpl,
230
+ sleep,
231
+ );
195
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
+ }
196
244
  if (batch.length < 100) break;
197
245
  }
198
- 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()]
199
255
  .filter(isStableRelease)
200
256
  .sort((a, b) => (a.published_at ?? "").localeCompare(b.published_at ?? ""));
201
257
  }
202
258
 
203
- async function fetchGitHubReleasesPage(
259
+ async function fetchGitHubReleasePayload<T>(
204
260
  url: string,
205
261
  headers: Record<string, string>,
206
262
  fetchImpl: typeof fetch,
207
263
  sleep: (delayMs: number) => Promise<void>,
208
- ): Promise<Response> {
264
+ ): Promise<T> {
209
265
  for (let attempt = 1; ; attempt++) {
210
266
  let response: Response;
211
267
  try {
212
268
  response = await fetchImpl(url, { headers });
269
+ // A successful fetch can still lose its connection while reading the body.
270
+ if (response.ok) return (await response.json()) as T;
213
271
  } catch (error) {
272
+ if (error instanceof SyntaxError) throw error;
214
273
  if (attempt === GITHUB_RELEASES_FETCH_ATTEMPTS) {
215
274
  throw new Error(
216
275
  `GitHub releases API request failed after ${attempt} attempts: ${String(error)}`,
@@ -221,8 +280,6 @@ async function fetchGitHubReleasesPage(
221
280
  continue;
222
281
  }
223
282
 
224
- if (response.ok) return response;
225
-
226
283
  const message = `GitHub releases API failed (${response.status}): ${await response.text()}`;
227
284
  if (response.status < 500 || attempt === GITHUB_RELEASES_FETCH_ATTEMPTS) {
228
285
  throw new Error(message);
@@ -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,
@@ -7,7 +7,7 @@ description: Comprehensive guide for initializing or reorganizing agent memory.
7
7
 
8
8
  Your memory is projected to a filesystem at `$MEMORY_DIR`, so you can restructure it with ordinary file tools and git. This skill applies the [Context Constitution](https://github.com/letta-ai/context-constitution/blob/main/constitution/CONSTITUTION.md)'s Progressive Disclosure and Efficiency principles to the [MemFS v2 root-first design](https://github.com/letta-ai/letta-code/blob/main/src/agent/prompts/letta_root_memfs.md).
9
9
 
10
- **You own this task through completion**: research, ask, synthesize, write, commit, verify. Delegate research, but integrate the results yourself — launching a background task is not completion. If a memory worker you launched is still editing this checkout, wait for it, then re-read before editing.
10
+ **You own this task through completion**: research, ask, synthesize, write, commit, verify. Delegate research, but integrate the results yourself — launching a background task is not completion.
11
11
 
12
12
  ## Principles
13
13
 
@@ -58,6 +58,8 @@ Discovery runs through the `MEMORY.md` chain, one level at a time:
58
58
 
59
59
  After adding or moving a file, trace the path a future request would take from root `MEMORY.md` to the file. If any step depends on knowing the file already exists, fix the index.
60
60
 
61
+ Indexes are the only way the agent finds deferred memory: it opens a file only when every entry on the path from root names what it is looking for. Keep them accurate as memory changes. Whenever you add, move, or substantially change what a directory holds, reread its entry in each parent `MEMORY.md` up to root and rewrite it to name the topics the directory now covers, in the words a future request would use. An entry written when the directory was new goes stale as it fills, and a topic no entry mentions is effectively lost.
62
+
61
63
  ## Design for progressive disclosure
62
64
 
63
65
  Each level should give the agent enough to decide whether to go deeper, and nothing it would have to read past to decide.
@@ -96,11 +96,13 @@ payload as a user message.
96
96
  unreadable receipt means deletion was accepted but remains unconfirmed; list
97
97
  webhooks before treating the capability URL as removed or retrying.
98
98
  - Normal update output additionally requires the returned webhook ID and
99
- `enabled` state to match the requested operation; normal rotate output
100
- requires the returned webhook ID to match. Canonical but contradictory 2xx
99
+ `enabled` state (or `target_device_id` for `update --computer`) to match the
100
+ requested operation; normal rotate output requires the returned webhook ID
101
+ to match. Canonical but contradictory 2xx
101
102
  records receive the same accepted/reconcile envelope and are never printed as
102
103
  ordinary success.
103
- - Normal create output also requires returned name and enabled state to match.
104
+ - Normal create output also requires returned name, enabled state, and
105
+ `target_device_id` to match.
104
106
  An explicitly supplied preprompt must round-trip exactly; when preprompt is
105
107
  omitted, the server-owned default is accepted only as a nonblank string, while
106
108
  missing, null, empty, whitespace-only, and non-string values are contradictory.
@@ -207,6 +209,24 @@ The caller sends `Authorization: Basic <base64("webhook:<security-key>")>`.
207
209
  Do not print that header or the key. The helper derives it internally for test
208
210
  requests when the same key is piped through stdin.
209
211
 
212
+ ### Run webhook turns on a connected computer
213
+
214
+ By default, webhook-triggered conversations run in the agent's Cloud sandbox.
215
+ To run them on a connected computer instead, get its device ID from
216
+ `letta computers list` and pass it with `--computer` on `create` or `update`:
217
+
218
+ ```bash
219
+ node <SKILL_DIR>/scripts/manage-agent-webhooks.mjs update \
220
+ --webhook-id webhook-agent-... \
221
+ --computer <deviceId>
222
+ ```
223
+
224
+ `list` shows the target as `target_device_id` (`null` means Cloud). If the
225
+ computer is offline when a request arrives, that delivery fails; it does not
226
+ fall back to Cloud. An unknown device ID returns `404 targetDeviceNotFound`.
227
+ Pass `--computer cloud` to `update` to clear the target and go back to the
228
+ Cloud sandbox.
229
+
210
230
  ### Test acceptance
211
231
 
212
232
  Public webhook:
@@ -15,6 +15,7 @@ const COMMANDS = new Set([
15
15
  "requests",
16
16
  "rotate",
17
17
  "test",
18
+ "update",
18
19
  ]);
19
20
  const BOOLEAN_FLAGS = new Set([
20
21
  "confirm",
@@ -28,6 +29,7 @@ const COMMAND_OPTIONS = new Map([
28
29
  [
29
30
  "create",
30
31
  new Set([
32
+ "computer",
31
33
  "credential-output",
32
34
  "disabled",
33
35
  "name",
@@ -53,12 +55,14 @@ const COMMAND_OPTIONS = new Map([
53
55
  "webhook-id",
54
56
  ]),
55
57
  ],
58
+ ["update", new Set(["computer", "webhook-id"])],
56
59
  ]);
57
60
 
58
61
  function usage() {
59
62
  console.error(`Usage:
60
63
  node manage-agent-webhooks.mjs list
61
- node manage-agent-webhooks.mjs create --name <name> (--public | --secure --credential-output <path> | --security-key-stdin) [--preprompt <text>] [--disabled]
64
+ node manage-agent-webhooks.mjs create --name <name> (--public | --secure --credential-output <path> | --security-key-stdin) [--preprompt <text>] [--disabled] [--computer <deviceId>]
65
+ node manage-agent-webhooks.mjs update --webhook-id <id> --computer <deviceId|cloud>
62
66
  node manage-agent-webhooks.mjs test --webhook-id <id> --confirm [--payload-file <path>] [--credential-file <path> | --security-key-stdin]
63
67
  node manage-agent-webhooks.mjs requests --webhook-id <id> [--limit <1-50>] [--include-body]
64
68
  node manage-agent-webhooks.mjs enable --webhook-id <id>
@@ -144,8 +148,10 @@ export function safeAgentWebhook(webhook) {
144
148
  for (const field of ["enabled", "requires_authorization_header"]) {
145
149
  if (typeof webhook[field] === "boolean") safe[field] = webhook[field];
146
150
  }
147
- if (typeof webhook.disabled_at === "string" || webhook.disabled_at === null) {
148
- safe.disabled_at = webhook.disabled_at;
151
+ for (const field of ["disabled_at", "target_device_id"]) {
152
+ if (typeof webhook[field] === "string" || webhook[field] === null) {
153
+ safe[field] = webhook[field];
154
+ }
149
155
  }
150
156
  return safe;
151
157
  }
@@ -753,6 +759,15 @@ function requireWebhookId(args) {
753
759
  return requireString(args["webhook-id"], "Pass --webhook-id");
754
760
  }
755
761
 
762
+ // "cloud" maps to null: run webhook turns in the agent's Cloud sandbox.
763
+ function targetDeviceIdFromArgs(args) {
764
+ const computer = requireString(
765
+ args.computer,
766
+ "--computer must be a device ID or cloud",
767
+ );
768
+ return computer === "cloud" ? null : computer;
769
+ }
770
+
756
771
  function requireConfirmation(args, action) {
757
772
  if (args.confirm !== true) {
758
773
  throw new Error(`${action} requires --confirm`);
@@ -827,6 +842,10 @@ async function runManagementCommand(params) {
827
842
  "--preprompt must contain a non-whitespace character",
828
843
  );
829
844
  }
845
+ if (params.args.computer !== undefined) {
846
+ const targetDeviceId = targetDeviceIdFromArgs(params.args);
847
+ if (targetDeviceId) body.target_device_id = targetDeviceId;
848
+ }
830
849
  if (securityKey) body.security_key = securityKey;
831
850
  const credentialReservation = usesGeneratedCredential
832
851
  ? await reserveCredentialFile(params.args["credential-output"])
@@ -914,6 +933,8 @@ async function runManagementCommand(params) {
914
933
  if (
915
934
  webhook.name !== body.name ||
916
935
  webhook.enabled !== body.enabled ||
936
+ (webhook.target_device_id ?? null) !==
937
+ (body.target_device_id ?? null) ||
917
938
  !prepromptMatches
918
939
  ) {
919
940
  throw new Error(
@@ -1048,14 +1069,22 @@ async function runManagementCommand(params) {
1048
1069
  return;
1049
1070
  }
1050
1071
 
1051
- if (params.command === "enable" || params.command === "disable") {
1072
+ if (
1073
+ params.command === "enable" ||
1074
+ params.command === "disable" ||
1075
+ params.command === "update"
1076
+ ) {
1052
1077
  if (params.command === "disable") {
1053
1078
  requireConfirmation(params.args, "Disabling a webhook");
1054
1079
  }
1080
+ const update =
1081
+ params.command === "update"
1082
+ ? { target_device_id: targetDeviceIdFromArgs(params.args) }
1083
+ : { enabled: params.command === "enable" };
1055
1084
  const mutation = await requestSideEffect(
1056
1085
  itemUrl,
1057
1086
  {
1058
- body: JSON.stringify({ enabled: params.command === "enable" }),
1087
+ body: JSON.stringify(update),
1059
1088
  headers: params.headers,
1060
1089
  method: "PATCH",
1061
1090
  },
@@ -1074,12 +1103,12 @@ async function runManagementCommand(params) {
1074
1103
  params.baseUrl,
1075
1104
  params.agentId,
1076
1105
  );
1077
- const expectedEnabled = params.command === "enable";
1078
- if (
1079
- webhook.id !== webhookId ||
1080
- typeof webhook.enabled !== "boolean" ||
1081
- webhook.enabled !== expectedEnabled
1082
- ) {
1106
+ const stateMatches =
1107
+ params.command === "update"
1108
+ ? webhook.target_device_id === update.target_device_id
1109
+ : typeof webhook.enabled === "boolean" &&
1110
+ webhook.enabled === update.enabled;
1111
+ if (webhook.id !== webhookId || !stateMatches) {
1083
1112
  throw new Error(
1084
1113
  "Update response does not match the requested webhook state",
1085
1114
  );
@@ -110,7 +110,7 @@ Use the active layout shown by the prompt and memory files. Do not create a
110
110
  `system/` directory in a root-layout repository or move existing-layout memory
111
111
  to the root as part of an unrelated self-configuration request.
112
112
 
113
- A requested memory or identity change is memory as the main task: make the edit directly with ordinary file tools, preserve the active layout, stage only the files you changed by explicit path, commit in the same shell call, and verify the result before reporting success. If a memory worker you launched may still be running, read its output file first (it ends with `[Task completed]` or `[Task failed]`) and reread the files before editing what it was asked to change. Delegate to the background `memory` subagent only when the change is incidental to another task the user is waiting on.
113
+ Before editing memory, load the relevant skills: `maintaining-core-memory` for root/core files, `maintaining-deferred-memory` for deferred files, and `creating-skills` for skills. Then make the edit directly with ordinary file tools so the agent with the context applies the memory guidance. Preserve the active layout, stage only changed files by explicit path, commit in the same shell call, and verify the result before reporting success.
114
114
 
115
115
  Do not use API system-prompt replacement for ordinary learning. That can clobber the compiled prompt. Edit the memory files instead.
116
116
 
@@ -5,15 +5,7 @@ description: Diagnose and repair MemFS repository setup, remote sync, authentica
5
5
 
6
6
  # MemFS Repository Repair
7
7
 
8
- Use this skill only when the Git repository behind an agent's memory is not
9
- setting up or syncing correctly. For ordinary memory reads and edits, use the
10
- memory files without loading this skill. Delegate incidental upkeep during another
11
- task to the background `memory` subagent. When memory repair is the user's main
12
- request, diagnose and repair it directly, then verify the result before reporting
13
- success. If a memory worker you launched may still be running, wait for it: re-read
14
- its output file until it ends with `[Task completed]` or `[Task failed]` before
15
- editing memory files or changing Git state. While the harness reports a repair in
16
- progress, leave both alone as well; that worker edits `$MEMORY_DIR` in place.
8
+ Use this skill when the Git repository behind an agent's memory is not setting up or syncing correctly. Before editing memory, load the relevant skills: `maintaining-core-memory` for root/core files, `maintaining-deferred-memory` for deferred files, and `creating-skills` for skills. Then make the edit directly with ordinary file tools so the agent with the context applies the memory guidance. Stage only changed files by explicit path and commit in the same shell call. Verify the repair before reporting success. While the harness reports a repair in progress, leave memory files and Git state to that repair worker, which edits `$MEMORY_DIR` in place.
17
9
 
18
10
  ## Current Model
19
11
 
@@ -122,7 +114,7 @@ cloud-backed agent's pending commits after the turn.
122
114
  The harness first tries a fast-forward pull. When a remote push is rejected
123
115
  because the remote moved, post-turn sync tries `git pull --rebase` and retries
124
116
  the push. If that rebase conflicts, the harness launches a fresh background
125
- `memory` subagent to repair it. The primary conversation is not interrupted.
117
+ internal `memory` subagent to repair it. The primary conversation is not interrupted.
126
118
  The harness retries normal sync after the worker finishes. The instructions
127
119
  below are for explicit troubleshooting; routine repairs run in the background.
128
120