@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.
- package/cursor-worker.js +167 -0
- package/dist/agent-presets.js +1 -64
- package/dist/agent-presets.js.map +2 -2
- package/dist/channels-slack.js +30 -24
- package/dist/channels-slack.js.map +3 -3
- package/dist/mcp-client.js +233 -75
- package/dist/mcp-client.js.map +9 -9
- package/dist/mcp-oauth.js +233 -75
- package/dist/mcp-oauth.js.map +9 -9
- package/dist/types/agent/subagents/index.d.ts +4 -0
- package/dist/types/agent/subagents/index.d.ts.map +1 -1
- package/dist/types/agent/subagents/manager.d.ts.map +1 -1
- package/dist/types/agent/subagents/memory-worker.d.ts +2 -8
- package/dist/types/agent/subagents/memory-worker.d.ts.map +1 -1
- package/dist/types/backend/api/providers.d.ts +8 -2
- package/dist/types/backend/api/providers.d.ts.map +1 -1
- package/dist/types/backend/api/schedules.d.ts +2 -2
- package/dist/types/backend/api/schedules.d.ts.map +1 -1
- package/dist/types/backend/backend.d.ts +5 -2
- package/dist/types/backend/backend.d.ts.map +1 -1
- package/dist/types/backend/dev/fake-headless-backend.d.ts +2 -1
- package/dist/types/backend/dev/fake-headless-backend.d.ts.map +1 -1
- package/dist/types/backend/dev/pi-model-factory.d.ts.map +1 -1
- package/dist/types/backend/local/local-provider-auth-store.d.ts +2 -0
- package/dist/types/backend/local/local-provider-auth-store.d.ts.map +1 -1
- package/dist/types/backend/local/local-transcript-history.d.ts +10 -0
- package/dist/types/backend/local/local-transcript-history.d.ts.map +1 -0
- package/dist/types/backend/local/local-transcript.d.ts +6 -0
- package/dist/types/backend/local/local-transcript.d.ts.map +1 -1
- package/dist/types/channels/slack/attachment-primitives.d.ts +2 -1
- package/dist/types/channels/slack/attachment-primitives.d.ts.map +1 -1
- package/dist/types/cli/subcommands/mcp.d.ts +2 -2
- package/dist/types/cli/subcommands/mcp.d.ts.map +1 -1
- package/dist/types/cron/process-identity.d.ts.map +1 -1
- package/dist/types/cron/run-log.d.ts +1 -0
- package/dist/types/cron/run-log.d.ts.map +1 -1
- package/dist/types/headless-environment-response.d.ts +27 -0
- package/dist/types/headless-environment-response.d.ts.map +1 -1
- package/dist/types/providers/byok-providers.d.ts +14 -0
- package/dist/types/providers/byok-providers.d.ts.map +1 -1
- package/dist/types/tools/impl/claude-stream-session.d.ts.map +1 -1
- package/dist/types/tools/impl/codex-app-server.d.ts.map +1 -1
- package/dist/types/tools/impl/cursor-session.d.ts +26 -0
- package/dist/types/tools/impl/cursor-session.d.ts.map +1 -0
- package/dist/types/tools/impl/cursor-worker.d.ts +32 -0
- package/dist/types/tools/impl/cursor-worker.d.ts.map +1 -0
- package/dist/types/tools/impl/external-coding-agent-task.d.ts.map +1 -1
- package/dist/types/tools/impl/external-coding-agent.d.ts +1 -1
- package/dist/types/tools/impl/external-coding-agent.d.ts.map +1 -1
- package/dist/types/tools/impl/memory-task-lifecycle.d.ts +2 -2
- package/dist/types/tools/impl/memory-task-lifecycle.d.ts.map +1 -1
- package/dist/types/tools/impl/remote-external-coding-agent.d.ts.map +1 -1
- package/dist/types/tools/impl/send-agent-message.d.ts.map +1 -1
- package/dist/types/tools/impl/task.d.ts.map +1 -1
- package/dist/types/tools/impl/wake.d.ts +4 -2
- package/dist/types/tools/impl/wake.d.ts.map +1 -1
- package/dist/types/tools/workflow/sdk-spawner.d.ts.map +1 -1
- package/dist/types/utils/native-cli-env.d.ts +3 -0
- package/dist/types/utils/native-cli-env.d.ts.map +1 -0
- package/dist/types/utils/process-liveness.d.ts +9 -0
- package/dist/types/utils/process-liveness.d.ts.map +1 -1
- package/letta.js +3553 -2911
- package/npm-shrinkwrap.json +291 -117
- package/package.json +5 -3
- package/scripts/agent-watch/verify-pr-identity.test.ts +148 -0
- package/scripts/agent-watch/verify-pr-identity.ts +72 -14
- package/scripts/claude-watch/agent-watch.ts +8 -2
- package/scripts/claude-watch/release-source.test.ts +120 -0
- package/scripts/claude-watch/release-source.ts +79 -9
- package/scripts/codex-watch/agent-watch.ts +2 -2
- package/scripts/codex-watch/release-analysis.test.ts +180 -4
- package/scripts/codex-watch/release-analysis.ts +65 -8
- package/scripts/source-file-size-baseline.json +5 -4
- package/skills/initializing-memory/SKILL.md +1 -1
- package/skills/maintaining-deferred-memory/SKILL.md +2 -0
- package/skills/managing-agent-webhooks/SKILL.md +23 -3
- package/skills/managing-agent-webhooks/scripts/manage-agent-webhooks.mjs +40 -11
- package/skills/self-configuration/SKILL.md +1 -1
- package/skills/syncing-memory-filesystem/SKILL.md +2 -10
- package/dist/types/agent/memory-worktree.d.ts +0 -83
- 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
|
|
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
|
|
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 =
|
|
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
|
|
194
|
-
|
|
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
|
|
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<
|
|
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":
|
|
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/
|
|
22
|
-
"src/
|
|
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":
|
|
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.
|
|
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
|
|
100
|
-
requires the returned webhook ID
|
|
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
|
|
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
|
-
|
|
148
|
-
|
|
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 (
|
|
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(
|
|
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
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|