@letta-ai/letta-agent-sdk 0.3.3 → 0.5.1

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 (56) hide show
  1. package/AGENTS.md +0 -3
  2. package/README.md +8 -1
  3. package/dist/app-server-management.d.ts +3 -48
  4. package/dist/app-server-management.d.ts.map +1 -1
  5. package/dist/app-server-session.d.ts +0 -6
  6. package/dist/app-server-session.d.ts.map +1 -1
  7. package/dist/client-base.d.ts +7 -15
  8. package/dist/client-base.d.ts.map +1 -1
  9. package/dist/client-entry.js +428 -486
  10. package/dist/client-entry.js.map +13 -12
  11. package/dist/client.d.ts +3 -3
  12. package/dist/client.d.ts.map +1 -1
  13. package/dist/cloud-session.d.ts +2 -0
  14. package/dist/cloud-session.d.ts.map +1 -1
  15. package/dist/cloud-status-transport.d.ts +53 -0
  16. package/dist/cloud-status-transport.d.ts.map +1 -0
  17. package/dist/index.d.ts +7 -16
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +911 -1993
  20. package/dist/index.js.map +18 -18
  21. package/dist/local-app-server-session.d.ts +1 -1
  22. package/dist/local-app-server-session.d.ts.map +1 -1
  23. package/dist/local-app-server.d.ts +11 -0
  24. package/dist/local-app-server.d.ts.map +1 -1
  25. package/dist/management.d.ts +0 -7
  26. package/dist/management.d.ts.map +1 -1
  27. package/dist/remote-session-protocol.d.ts +2 -0
  28. package/dist/remote-session-protocol.d.ts.map +1 -1
  29. package/dist/remote-turn-coordinator.d.ts.map +1 -1
  30. package/dist/types.d.ts +65 -66
  31. package/dist/types.d.ts.map +1 -1
  32. package/dist/validation.d.ts.map +1 -1
  33. package/package.json +2 -2
  34. package/src/app-server-management.ts +20 -256
  35. package/src/app-server-session.ts +0 -32
  36. package/src/client-base.ts +47 -73
  37. package/src/client.ts +31 -70
  38. package/src/cloud-session.ts +113 -287
  39. package/src/cloud-status-transport.ts +305 -0
  40. package/src/index.ts +11 -29
  41. package/src/local-app-server-session.ts +13 -3
  42. package/src/local-app-server.ts +97 -3
  43. package/src/management.ts +0 -7
  44. package/src/remote-session-protocol.ts +18 -0
  45. package/src/remote-turn-coordinator.ts +20 -2
  46. package/src/types.ts +72 -111
  47. package/src/validation.ts +21 -6
  48. package/dist/protocol.d.ts +0 -205
  49. package/dist/protocol.d.ts.map +0 -1
  50. package/dist/session.d.ts +0 -155
  51. package/dist/session.d.ts.map +0 -1
  52. package/dist/transport.d.ts +0 -53
  53. package/dist/transport.d.ts.map +0 -1
  54. package/src/protocol.ts +0 -249
  55. package/src/session.ts +0 -1638
  56. package/src/transport.ts +0 -484
@@ -0,0 +1,305 @@
1
+ import type {
2
+ AppServerSocketConstructor,
3
+ AppServerSocketLike,
4
+ AppServerSocketOptions,
5
+ } from "@letta-ai/letta-code/app-server-client";
6
+ import type { ProtocolMessage, RuntimeScope } from "./remote-session-protocol.js";
7
+ import type {
8
+ LettaCodeSocketConstructor,
9
+ LettaCodeSocketLike,
10
+ } from "./types.js";
11
+
12
+ type CloudStatusMessage = ProtocolMessage & {
13
+ type: string;
14
+ seq?: unknown;
15
+ event_seq?: unknown;
16
+ idempotency_key?: unknown;
17
+ };
18
+
19
+ type CloudStatusTransportOptions = {
20
+ url: string;
21
+ WebSocket: LettaCodeSocketConstructor;
22
+ headers?: Record<string, string>;
23
+ pingIntervalMs: number;
24
+ runtime: RuntimeScope;
25
+ };
26
+
27
+ type SocketChannel = "control" | "stream";
28
+ type Listener = (event: unknown) => void;
29
+
30
+ const CONNECTING = 0;
31
+ const OPEN = 1;
32
+ const CLOSING = 2;
33
+ const CLOSED = 3;
34
+ const MAX_IDEMPOTENCY_KEYS = 1_000;
35
+
36
+ function addSocketListener(
37
+ socket: LettaCodeSocketLike,
38
+ type: string,
39
+ listener: Listener,
40
+ ): () => void {
41
+ if (socket.addEventListener) {
42
+ socket.addEventListener(type, listener);
43
+ return () => socket.removeEventListener?.(type, listener);
44
+ }
45
+ if (socket.on) {
46
+ socket.on(type, listener);
47
+ return () => socket.off?.(type, listener);
48
+ }
49
+ throw new Error("WebSocket implementation does not support event listeners.");
50
+ }
51
+
52
+ function messageEventData(event: unknown): string | null {
53
+ if (typeof event === "string") return event;
54
+ if (!event || typeof event !== "object") return null;
55
+ const data = (event as { data?: unknown }).data;
56
+ if (typeof data === "string") return data;
57
+ if (data instanceof ArrayBuffer) {
58
+ return new TextDecoder().decode(data);
59
+ }
60
+ if (data instanceof Uint8Array) {
61
+ return new TextDecoder().decode(data);
62
+ }
63
+ return null;
64
+ }
65
+
66
+ function channelUrl(url: string, channel: SocketChannel): string {
67
+ const parsed = new URL(url);
68
+ parsed.searchParams.set("channel", channel);
69
+ return parsed.toString();
70
+ }
71
+
72
+ /**
73
+ * Cloud's status relay still exposes separate control and stream sockets.
74
+ *
75
+ * This adapter presents those two relay sockets as one AppServerSocketLike,
76
+ * allowing CloudSession to reuse AppServerClient's request correlation and
77
+ * the SDK's shared runtime controller without weakening the local app-server's
78
+ * strict one-socket contract. Remove it when LET-10236 migrates the relay.
79
+ */
80
+ export class CloudStatusTransport implements AppServerSocketLike {
81
+ readonly controlSocket: LettaCodeSocketLike;
82
+ readonly streamSocket: LettaCodeSocketLike;
83
+
84
+ private state = CONNECTING;
85
+ private openedChannels = new Set<SocketChannel>();
86
+ private listeners = new Map<string, Set<Listener>>();
87
+ private removers: Array<() => void> = [];
88
+ private seenIdempotencyKeys = new Set<string>();
89
+ private seenIdempotencyOrder: string[] = [];
90
+ private lastEventSeq: number | null = null;
91
+ private pingTimer: ReturnType<typeof setInterval> | null = null;
92
+ private closeEmitted = false;
93
+
94
+ constructor(private readonly options: CloudStatusTransportOptions) {
95
+ const socketOptions = options.headers
96
+ ? { headers: options.headers }
97
+ : undefined;
98
+ this.controlSocket = new options.WebSocket(
99
+ channelUrl(options.url, "control"),
100
+ socketOptions,
101
+ );
102
+ this.streamSocket = new options.WebSocket(
103
+ channelUrl(options.url, "stream"),
104
+ socketOptions,
105
+ );
106
+ this.bindSocket("control", this.controlSocket);
107
+ this.bindSocket("stream", this.streamSocket);
108
+ this.startPing();
109
+ }
110
+
111
+ get readyState(): number {
112
+ return this.state;
113
+ }
114
+
115
+ send(data: string): void {
116
+ if (this.state !== OPEN || this.controlSocket.readyState !== OPEN) {
117
+ throw new Error("Cloud status control socket is not open");
118
+ }
119
+ this.controlSocket.send(data);
120
+ }
121
+
122
+ close(): void {
123
+ if (this.state === CLOSED || this.state === CLOSING) return;
124
+ this.state = CLOSING;
125
+ this.stopPing();
126
+ this.closeUnderlyingSockets();
127
+ this.finishClose({});
128
+ }
129
+
130
+ addEventListener(type: string, listener: Listener): void {
131
+ let listeners = this.listeners.get(type);
132
+ if (!listeners) {
133
+ listeners = new Set();
134
+ this.listeners.set(type, listeners);
135
+ }
136
+ listeners.add(listener);
137
+ }
138
+
139
+ removeEventListener(type: string, listener: Listener): void {
140
+ this.listeners.get(type)?.delete(listener);
141
+ }
142
+
143
+ private bindSocket(
144
+ channel: SocketChannel,
145
+ socket: LettaCodeSocketLike,
146
+ ): void {
147
+ this.removers.push(
148
+ addSocketListener(socket, "open", () => {
149
+ this.openedChannels.add(channel);
150
+ if (this.openedChannels.size === 2 && this.state === CONNECTING) {
151
+ this.state = OPEN;
152
+ this.emit("open", {});
153
+ }
154
+ }),
155
+ addSocketListener(socket, "message", (event) => {
156
+ if (this.shouldForwardMessage(channel, socket, event)) {
157
+ this.emit("message", event);
158
+ }
159
+ }),
160
+ addSocketListener(socket, "error", (event) => {
161
+ this.emit("error", event);
162
+ }),
163
+ addSocketListener(socket, "close", (event) => {
164
+ if (this.state !== CLOSED) {
165
+ this.state = CLOSING;
166
+ this.stopPing();
167
+ this.closeUnderlyingSockets(socket);
168
+ this.finishClose(event);
169
+ }
170
+ }),
171
+ );
172
+ }
173
+
174
+ private shouldForwardMessage(
175
+ channel: SocketChannel,
176
+ socket: LettaCodeSocketLike,
177
+ event: unknown,
178
+ ): boolean {
179
+ const data = messageEventData(event);
180
+ if (!data) return true;
181
+ let message: CloudStatusMessage;
182
+ try {
183
+ message = JSON.parse(data) as CloudStatusMessage;
184
+ } catch {
185
+ return true;
186
+ }
187
+
188
+ this.ackIfSequenced(socket, message);
189
+ if (channel === "control" && message.type === "stream_delta") {
190
+ return false;
191
+ }
192
+ if (this.isDuplicate(message)) return false;
193
+ this.trackEventSequence(message);
194
+ return true;
195
+ }
196
+
197
+ private ackIfSequenced(
198
+ socket: LettaCodeSocketLike,
199
+ message: CloudStatusMessage,
200
+ ): void {
201
+ if (typeof message.seq !== "number") return;
202
+ this.sendCommand(socket, { type: "ack", seq: message.seq });
203
+ }
204
+
205
+ private isDuplicate(message: CloudStatusMessage): boolean {
206
+ const key =
207
+ typeof message.idempotency_key === "string"
208
+ ? message.idempotency_key
209
+ : null;
210
+ if (!key) return false;
211
+ if (this.seenIdempotencyKeys.has(key)) return true;
212
+ this.seenIdempotencyKeys.add(key);
213
+ this.seenIdempotencyOrder.push(key);
214
+ while (this.seenIdempotencyOrder.length > MAX_IDEMPOTENCY_KEYS) {
215
+ const oldest = this.seenIdempotencyOrder.shift();
216
+ if (oldest) this.seenIdempotencyKeys.delete(oldest);
217
+ }
218
+ return false;
219
+ }
220
+
221
+ private trackEventSequence(message: CloudStatusMessage): void {
222
+ if (typeof message.event_seq !== "number") return;
223
+ if (
224
+ this.lastEventSeq !== null &&
225
+ message.event_seq > this.lastEventSeq + 1
226
+ ) {
227
+ this.sendCommand(this.controlSocket, {
228
+ type: "sync",
229
+ runtime: this.options.runtime,
230
+ recover_approvals: true,
231
+ force_device_status: true,
232
+ });
233
+ }
234
+ if (
235
+ this.lastEventSeq === null ||
236
+ message.event_seq > this.lastEventSeq
237
+ ) {
238
+ this.lastEventSeq = message.event_seq;
239
+ }
240
+ }
241
+
242
+ private sendCommand(
243
+ socket: LettaCodeSocketLike,
244
+ command: Record<string, unknown>,
245
+ ): void {
246
+ if (socket.readyState !== OPEN) return;
247
+ try {
248
+ socket.send(JSON.stringify(command));
249
+ } catch {
250
+ // ACK, sync recovery, and ping are best-effort relay reliability frames.
251
+ }
252
+ }
253
+
254
+ private startPing(): void {
255
+ this.pingTimer = setInterval(() => {
256
+ this.sendCommand(this.controlSocket, { type: "ping" });
257
+ this.sendCommand(this.streamSocket, { type: "ping" });
258
+ }, this.options.pingIntervalMs);
259
+ (this.pingTimer as { unref?: () => void }).unref?.();
260
+ }
261
+
262
+ private stopPing(): void {
263
+ if (!this.pingTimer) return;
264
+ clearInterval(this.pingTimer);
265
+ this.pingTimer = null;
266
+ }
267
+
268
+ private closeUnderlyingSockets(
269
+ except?: LettaCodeSocketLike,
270
+ ): void {
271
+ for (const socket of [this.controlSocket, this.streamSocket]) {
272
+ if (
273
+ socket !== except &&
274
+ (socket.readyState === CONNECTING || socket.readyState === OPEN)
275
+ ) {
276
+ socket.close();
277
+ }
278
+ }
279
+ }
280
+
281
+ private finishClose(event: unknown): void {
282
+ if (this.closeEmitted) return;
283
+ this.closeEmitted = true;
284
+ this.state = CLOSED;
285
+ this.stopPing();
286
+ for (const remove of this.removers.splice(0)) remove();
287
+ this.emit("close", event);
288
+ }
289
+
290
+ private emit(type: string, event: unknown): void {
291
+ for (const listener of this.listeners.get(type) ?? []) {
292
+ listener(event);
293
+ }
294
+ }
295
+ }
296
+
297
+ export function createCloudStatusTransportConstructor(
298
+ options: CloudStatusTransportOptions,
299
+ ): AppServerSocketConstructor {
300
+ return class CloudStatusTransportSocket extends CloudStatusTransport {
301
+ constructor(_url: string, _socketOptions?: AppServerSocketOptions) {
302
+ super(options);
303
+ }
304
+ };
305
+ }
package/src/index.ts CHANGED
@@ -11,9 +11,6 @@
11
11
  * const agentId = await client.createAgent();
12
12
  * const clientSession = client.resumeSession(agentId);
13
13
  *
14
- * // Start session with default agent + new conversation (like `letta`)
15
- * const session = createSession();
16
- *
17
14
  * // Create a new agent explicitly
18
15
  * const agentId = await createAgent();
19
16
  *
@@ -26,13 +23,11 @@
26
23
  * // Create new conversation on specific agent
27
24
  * const session = createSession(agentId);
28
25
  *
29
- * // One-shot prompt (uses default agent)
30
- * const result = await prompt('Hello');
31
- * const result = await prompt('Hello', agentId); // specific agent
26
+ * // One-shot prompt in a new conversation
27
+ * const result = await prompt('Hello', agentId);
32
28
  * ```
33
29
  */
34
30
 
35
- import { Session } from "./session.js";
36
31
  import { LettaAgentClient } from "./client.js";
37
32
  import type {
38
33
  CreateSessionOptions,
@@ -98,6 +93,7 @@ export type {
98
93
  SessionDiffPreview,
99
94
  SkillSource,
100
95
  DreamingOptions,
96
+ SessionDreamingOptions,
101
97
  DreamingTrigger,
102
98
  DreamingBehavior,
103
99
  EffectiveDreamingSettings,
@@ -164,7 +160,6 @@ export type {
164
160
  ConversationMessagesResult,
165
161
  } from "./management-types.js";
166
162
 
167
- export { Session } from "./session.js";
168
163
  export { RepositoriesClient } from "./repositories.js";
169
164
  export { LettaAgentClient } from "./client.js";
170
165
  export { CloudManagedSandboxExpiredError } from "./cloud-session.js";
@@ -210,29 +205,20 @@ export async function createAgent(options: CreateAgentOptions = {}): Promise<str
210
205
  /**
211
206
  * Create a new conversation (session).
212
207
  *
213
- * - Without agentId: uses default/LRU agent with new conversation (like `letta`)
214
- * - With agentId: creates new conversation on specified agent
208
+ * Creates a new conversation on the specified agent.
215
209
  *
216
210
  * @example
217
211
  * ```typescript
218
- * // New conversation on default agent (like `letta`)
219
- * await using session = createSession();
220
- *
221
212
  * // New conversation on specific agent
222
213
  * await using session = createSession(agentId);
223
214
  * ```
224
215
  */
225
216
  export function createSession(
226
- agentId?: string,
217
+ agentId: string,
227
218
  options: CreateSessionOptions = {},
228
219
  ): LettaCodeSession {
229
220
  validateCreateSessionOptions(options);
230
- if (agentId) {
231
- return new LettaAgentClient().createSession(agentId, options);
232
- }
233
- // The app-server runtime_start protocol requires an explicit agent id. Keep
234
- // the historical default/LRU-agent helper on the legacy stdio transport.
235
- return new LettaAgentClient({ backend: "local", transport: "stdio" }).createSession(options);
221
+ return new LettaAgentClient().createSession(agentId, options);
236
222
  }
237
223
 
238
224
  /**
@@ -264,13 +250,11 @@ export function resumeSession(
264
250
  /**
265
251
  * One-shot prompt convenience function.
266
252
  *
267
- * - Without agentId: uses default agent (like `letta -p`), new conversation
268
- * - With agentId: uses specific agent, new conversation
253
+ * Uses the specified agent in a new conversation.
269
254
  * - Uses a short-lived session and returns the final turn result.
270
255
  *
271
256
  * @example
272
257
  * ```typescript
273
- * const result = await prompt('What is 2+2?'); // default agent
274
258
  * const result = await prompt('What is the capital of France?', agentId); // specific agent
275
259
  * ```
276
260
  */
@@ -283,13 +267,11 @@ type InitializableSession = LettaCodeSession & {
283
267
  };
284
268
 
285
269
  export async function prompt(
286
- message: string,
287
- agentId?: string
270
+ message: SendMessage,
271
+ agentId: string,
272
+ options: CreateSessionOptions = {},
288
273
  ): Promise<SDKResultMessage> {
289
- // Use default agent behavior (like letta -p) when no agentId specified
290
- const session = agentId
291
- ? createSession(agentId)
292
- : createSession();
274
+ const session = createSession(agentId, options);
293
275
 
294
276
  try {
295
277
  return await (session as TurnSession).runTurn(message);
@@ -4,12 +4,14 @@ import {
4
4
  type AppServerSessionOptions,
5
5
  } from "./app-server-session.js";
6
6
  import { startLocalAppServer } from "./local-app-server.js";
7
- import type { LettaCodeLocalAppServerOptions } from "./types.js";
7
+ import type {
8
+ LettaCodeClientSessionOptions,
9
+ LettaCodeLocalAppServerOptions,
10
+ } from "./types.js";
8
11
 
9
12
  export function createLocalAppServerSession(
10
13
  options: LettaCodeLocalAppServerOptions | undefined,
11
14
  mode: AppServerSessionMode,
12
- beforeConnect?: () => Promise<void>,
13
15
  ): AppServerSession {
14
16
  const appServer = options ?? {};
15
17
  const sessionOptions: AppServerSessionOptions = {
@@ -22,6 +24,15 @@ export function createLocalAppServerSession(
22
24
  backend: appServer.harnessBackend ?? "local",
23
25
  startupTimeoutMs: appServer.startupTimeoutMs,
24
26
  env: sessionEnv,
27
+ filesystemConfinement:
28
+ mode.kind === "session"
29
+ ? (mode.options as LettaCodeClientSessionOptions)
30
+ .filesystemConfinement
31
+ : undefined,
32
+ agentId:
33
+ mode.kind === "session" && "agentId" in mode
34
+ ? mode.agentId
35
+ : undefined,
25
36
  }),
26
37
  }),
27
38
  ...(appServer.WebSocket !== undefined
@@ -33,7 +44,6 @@ export function createLocalAppServerSession(
33
44
  ...(appServer.pinGlobalAgent !== undefined
34
45
  ? { pinGlobalAgent: appServer.pinGlobalAgent }
35
46
  : {}),
36
- ...(beforeConnect ? { beforeConnect } : {}),
37
47
  };
38
48
  return new AppServerSession(sessionOptions, mode);
39
49
  }
@@ -1,4 +1,11 @@
1
1
  import { spawn, type ChildProcess } from "node:child_process";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+ import {
5
+ createMemoryConfinementLauncher,
6
+ type MemoryConfinementLauncherInput,
7
+ type MemoryConfinementLauncherResult,
8
+ } from "@letta-ai/letta-code/memory-confinement";
2
9
  import { findLettaCli } from "./cli-resolver.js";
3
10
 
4
11
  export interface LocalAppServerHandle {
@@ -12,12 +19,70 @@ export interface StartLocalAppServerOptions {
12
19
  startupTimeoutMs?: number;
13
20
  cliPath?: string;
14
21
  env?: Record<string, string | undefined>;
22
+ filesystemConfinement?: "memory";
23
+ agentId?: string;
15
24
  }
16
25
 
26
+ interface LocalAppServerProcess {
27
+ command: string;
28
+ args: string[];
29
+ env: NodeJS.ProcessEnv;
30
+ }
31
+
32
+ type MemoryConfinementLauncher = (
33
+ input: MemoryConfinementLauncherInput,
34
+ ) => MemoryConfinementLauncherResult;
35
+
17
36
  const DEFAULT_LISTEN_URL = "ws://127.0.0.1:0";
18
37
  const DEFAULT_STARTUP_TIMEOUT_MS = 30_000;
19
38
  const LISTENING_RE = /^Listening on\s+(ws:\/\/\S+)\s*$/m;
20
39
 
40
+ function targetHomeDirectory(env: NodeJS.ProcessEnv): string {
41
+ if (process.platform === "win32") {
42
+ const profile = env.USERPROFILE?.trim();
43
+ if (profile) return profile;
44
+ const drive = env.HOMEDRIVE?.trim();
45
+ const path = env.HOMEPATH?.trim();
46
+ if (drive && path) return `${drive}${path}`;
47
+ return homedir();
48
+ }
49
+ return env.HOME?.trim() || homedir();
50
+ }
51
+
52
+ function withDefaultMemoryDirectory(
53
+ env: NodeJS.ProcessEnv,
54
+ options: StartLocalAppServerOptions,
55
+ ): NodeJS.ProcessEnv {
56
+ const explicitMemoryDir = options.env?.MEMORY_DIR?.trim();
57
+ const explicitLettaMemoryDir = options.env?.LETTA_MEMORY_DIR?.trim();
58
+ const scopedEnv = { ...env };
59
+ if (explicitMemoryDir || explicitLettaMemoryDir) {
60
+ if (!explicitMemoryDir) delete scopedEnv.MEMORY_DIR;
61
+ if (!explicitLettaMemoryDir) delete scopedEnv.LETTA_MEMORY_DIR;
62
+ return scopedEnv;
63
+ }
64
+
65
+ // Never inherit the SDK process's agent scope into a different session.
66
+ delete scopedEnv.MEMORY_DIR;
67
+ delete scopedEnv.LETTA_MEMORY_DIR;
68
+ if (!options.agentId) return scopedEnv;
69
+
70
+ const homeDir = targetHomeDirectory(env);
71
+ // Mirrors Letta Code's getScopedMemoryFilesystemRoot contract. The SDK pins
72
+ // an exact Letta Code version so the launch policy and layout move together.
73
+ const memoryDir =
74
+ options.backend === "api"
75
+ ? join(homeDir, ".letta", "agents", options.agentId, "memory")
76
+ : join(
77
+ env.LETTA_LOCAL_BACKEND_DIR?.trim() ||
78
+ join(homeDir, ".letta", "lc-local-backend"),
79
+ "memfs",
80
+ options.agentId,
81
+ "memory",
82
+ );
83
+ return { ...scopedEnv, MEMORY_DIR: memoryDir };
84
+ }
85
+
21
86
  function appendLine(buffer: string, chunk: unknown): string {
22
87
  return buffer + String(chunk);
23
88
  }
@@ -40,6 +105,35 @@ export function buildLocalAppServerArgs(
40
105
  ];
41
106
  }
42
107
 
108
+ export function buildLocalAppServerProcess(
109
+ cliPath: string,
110
+ options: StartLocalAppServerOptions = {},
111
+ confineMemory: MemoryConfinementLauncher = createMemoryConfinementLauncher,
112
+ ): LocalAppServerProcess {
113
+ const env = { ...process.env, ...(options.env ?? {}) };
114
+ const launcher = [
115
+ process.execPath,
116
+ ...buildLocalAppServerArgs(cliPath, options),
117
+ ];
118
+ if (options.filesystemConfinement !== "memory") {
119
+ return {
120
+ command: launcher[0] as string,
121
+ args: launcher.slice(1),
122
+ env,
123
+ };
124
+ }
125
+
126
+ const confined = confineMemory({
127
+ launcher,
128
+ env: withDefaultMemoryDirectory(env, options),
129
+ });
130
+ return {
131
+ command: confined.launcher[0] as string,
132
+ args: confined.launcher.slice(1),
133
+ env: confined.env,
134
+ };
135
+ }
136
+
43
137
  function terminateProcess(child: ChildProcess): void {
44
138
  if (child.exitCode !== null || child.signalCode !== null) return;
45
139
  child.kill("SIGTERM");
@@ -57,13 +151,13 @@ export function startLocalAppServer(
57
151
  options: StartLocalAppServerOptions = {},
58
152
  ): Promise<LocalAppServerHandle> {
59
153
  const cliPath = options.cliPath ?? findLettaCli();
60
- const args = buildLocalAppServerArgs(cliPath, options);
154
+ const processSpec = buildLocalAppServerProcess(cliPath, options);
61
155
  const startupTimeoutMs = options.startupTimeoutMs ?? DEFAULT_STARTUP_TIMEOUT_MS;
62
156
 
63
157
  return new Promise((resolve, reject) => {
64
- const child = spawn(process.execPath, args, {
158
+ const child = spawn(processSpec.command, processSpec.args, {
65
159
  stdio: ["ignore", "pipe", "pipe"],
66
- env: { ...process.env, ...(options.env ?? {}) },
160
+ env: processSpec.env,
67
161
  });
68
162
 
69
163
  let settled = false;
package/src/management.ts CHANGED
@@ -45,13 +45,6 @@ export interface ManagementTransport {
45
45
  conversationId: string,
46
46
  query: ManagementQuery,
47
47
  ): Promise<ConversationMessagesResult>;
48
- /**
49
- * Release the pooled connection, waiting for in-flight work when necessary,
50
- * so other clients of the same backend (for example sessions, which compete
51
- * for the app-server's single control-client slot) can connect. Optional;
52
- * transports whose backend has no such contention (Cloud REST) omit it.
53
- */
54
- releaseIdleConnection?(): Promise<void> | void;
55
48
  }
56
49
 
57
50
  type TransportProvider = () => ManagementTransport;
@@ -450,6 +450,20 @@ export function streamDeltaRunId(delta: Record<string, unknown>): string | undef
450
450
  return typeof delta.run_id === "string" ? delta.run_id : undefined;
451
451
  }
452
452
 
453
+ export function streamDeltaOtid(
454
+ delta: Record<string, unknown>,
455
+ ): string | null | undefined {
456
+ return typeof delta.otid === "string" || delta.otid === null
457
+ ? delta.otid
458
+ : undefined;
459
+ }
460
+
461
+ export function streamDeltaSeqId(
462
+ delta: Record<string, unknown>,
463
+ ): number | undefined {
464
+ return typeof delta.seq_id === "number" ? delta.seq_id : undefined;
465
+ }
466
+
453
467
  export function streamDeltaStopReason(delta: Record<string, unknown>): string | null | undefined {
454
468
  return typeof delta.stop_reason === "string" ? delta.stop_reason : undefined;
455
469
  }
@@ -650,6 +664,10 @@ export function toSessionDeviceStatus(
650
664
  isProcessing: status.is_processing,
651
665
  permissionMode,
652
666
  workingDirectory: status.current_working_directory,
667
+ memoryDirectory:
668
+ typeof status.memory_directory === "string"
669
+ ? status.memory_directory
670
+ : null,
653
671
  pendingControlRequests: pendingControlRequests(status),
654
672
  raw: { ...status },
655
673
  };
@@ -18,8 +18,10 @@ import {
18
18
  queueItems,
19
19
  sameRuntime,
20
20
  streamDeltaMessageType,
21
+ streamDeltaOtid,
21
22
  streamDeltaRecord,
22
23
  streamDeltaRunId,
24
+ streamDeltaSeqId,
23
25
  streamDeltaStopReason,
24
26
  toSdkErrorCode,
25
27
  toSessionDeviceStatus,
@@ -303,6 +305,8 @@ export class RemoteTurnCoordinator {
303
305
  const messageType =
304
306
  typeof delta.message_type === "string" ? delta.message_type : undefined;
305
307
  const runId = typeof delta.run_id === "string" ? delta.run_id : undefined;
308
+ const otid = streamDeltaOtid(delta);
309
+ const seqId = streamDeltaSeqId(delta);
306
310
  const uuid =
307
311
  typeof delta.id === "string"
308
312
  ? delta.id
@@ -312,7 +316,14 @@ export class RemoteTurnCoordinator {
312
316
  const content = extractTextFromContent(delta.content);
313
317
  if (!content) return null;
314
318
  if (this.activeTurn) this.activeTurn.assistantText += content;
315
- return { type: "assistant", content, uuid, runId };
319
+ return {
320
+ type: "assistant",
321
+ content,
322
+ uuid,
323
+ ...(otid !== undefined ? { otid } : {}),
324
+ ...(seqId !== undefined ? { seqId } : {}),
325
+ runId,
326
+ };
316
327
  }
317
328
 
318
329
  if (messageType === "reasoning_message") {
@@ -321,7 +332,14 @@ export class RemoteTurnCoordinator {
321
332
  ? delta.reasoning
322
333
  : extractTextFromContent(delta.content);
323
334
  if (!content) return null;
324
- return { type: "reasoning", content, uuid, runId };
335
+ return {
336
+ type: "reasoning",
337
+ content,
338
+ uuid,
339
+ ...(otid !== undefined ? { otid } : {}),
340
+ ...(seqId !== undefined ? { seqId } : {}),
341
+ runId,
342
+ };
325
343
  }
326
344
 
327
345
  if (