@letta-ai/letta-code 0.32.12 → 0.32.14

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 (137) hide show
  1. package/dist/agent-presets.js +1 -1
  2. package/dist/agent-presets.js.map +3 -3
  3. package/dist/channels-public.js +7 -7
  4. package/dist/channels-public.js.map +3 -3
  5. package/dist/channels-slack.js.map +2 -2
  6. package/dist/gateway-core.js +7 -7
  7. package/dist/gateway-core.js.map +3 -3
  8. package/dist/mcp-client.js +2 -2
  9. package/dist/mcp-client.js.map +1 -1
  10. package/dist/types/agent/approval-execution.d.ts.map +1 -1
  11. package/dist/types/agent/memory-filesystem.d.ts +3 -4
  12. package/dist/types/agent/memory-filesystem.d.ts.map +1 -1
  13. package/dist/types/agent/prompt-assets.d.ts +0 -1
  14. package/dist/types/agent/prompt-assets.d.ts.map +1 -1
  15. package/dist/types/agent/subagents/manager.d.ts.map +1 -1
  16. package/dist/types/agent/subagents/names.d.ts +1 -1
  17. package/dist/types/agent/subagents/names.d.ts.map +1 -1
  18. package/dist/types/agent/subagents/subagent-launcher.d.ts +2 -0
  19. package/dist/types/agent/subagents/subagent-launcher.d.ts.map +1 -1
  20. package/dist/types/agent/subagents/subagent-process.d.ts +23 -0
  21. package/dist/types/agent/subagents/subagent-process.d.ts.map +1 -0
  22. package/dist/types/agent/subagents/subagent-stream.d.ts +3 -0
  23. package/dist/types/agent/subagents/subagent-stream.d.ts.map +1 -1
  24. package/dist/types/backend/api/reflection-runs.d.ts +17 -0
  25. package/dist/types/backend/api/reflection-runs.d.ts.map +1 -0
  26. package/dist/types/backend/api/reflection.d.ts +22 -0
  27. package/dist/types/backend/api/reflection.d.ts.map +1 -0
  28. package/dist/types/backend/backend.d.ts +16 -0
  29. package/dist/types/backend/backend.d.ts.map +1 -1
  30. package/dist/types/backend/dev/headless-turn-executor.d.ts +8 -0
  31. package/dist/types/backend/dev/headless-turn-executor.d.ts.map +1 -1
  32. package/dist/types/backend/dev/pi-stream-adapter.d.ts.map +1 -1
  33. package/dist/types/backend/dev/pi-stream-output.d.ts +3 -0
  34. package/dist/types/backend/dev/pi-stream-output.d.ts.map +1 -0
  35. package/dist/types/backend/dev/provider-turn-executor.d.ts.map +1 -1
  36. package/dist/types/backend/local/local-backend.d.ts.map +1 -1
  37. package/dist/types/backend/local/local-executor-factory.d.ts +1 -1
  38. package/dist/types/backend/local/local-executor-factory.d.ts.map +1 -1
  39. package/dist/types/backend/local/local-message.d.ts +2 -1
  40. package/dist/types/backend/local/local-message.d.ts.map +1 -1
  41. package/dist/types/backend/local/local-store.d.ts.map +1 -1
  42. package/dist/types/backend/local/local-stream-chunks.d.ts +13 -0
  43. package/dist/types/backend/local/local-stream-chunks.d.ts.map +1 -1
  44. package/dist/types/channels/progress-formatting.d.ts.map +1 -1
  45. package/dist/types/cli/helpers/stream-processor.d.ts +7 -2
  46. package/dist/types/cli/helpers/stream-processor.d.ts.map +1 -1
  47. package/dist/types/cli/helpers/stream.d.ts +7 -5
  48. package/dist/types/cli/helpers/stream.d.ts.map +1 -1
  49. package/dist/types/cli/helpers/tool-name-mapping.d.ts +1 -2
  50. package/dist/types/cli/helpers/tool-name-mapping.d.ts.map +1 -1
  51. package/dist/types/hooks/executor.d.ts +5 -0
  52. package/dist/types/hooks/executor.d.ts.map +1 -1
  53. package/dist/types/hooks/index.d.ts.map +1 -1
  54. package/dist/types/permissions/canonical.d.ts.map +1 -1
  55. package/dist/types/permissions/checker.d.ts.map +1 -1
  56. package/dist/types/permissions/cross-agent-guard.d.ts +1 -1
  57. package/dist/types/permissions/cross-agent-guard.d.ts.map +1 -1
  58. package/dist/types/permissions/mode.d.ts.map +1 -1
  59. package/dist/types/permissions/workspace-sandbox.d.ts.map +1 -1
  60. package/dist/types/providers/byok-providers.d.ts +1 -0
  61. package/dist/types/providers/byok-providers.d.ts.map +1 -1
  62. package/dist/types/settings-manager.d.ts.map +1 -1
  63. package/dist/types/telemetry/index.d.ts +16 -1
  64. package/dist/types/telemetry/index.d.ts.map +1 -1
  65. package/dist/types/tools/filter.d.ts +0 -10
  66. package/dist/types/tools/filter.d.ts.map +1 -1
  67. package/dist/types/tools/impl/bash-output.d.ts.map +1 -1
  68. package/dist/types/tools/impl/bash.d.ts.map +1 -1
  69. package/dist/types/tools/impl/enter-worktree.d.ts.map +1 -1
  70. package/dist/types/tools/impl/exec-command.d.ts.map +1 -1
  71. package/dist/types/tools/impl/process_manager.d.ts +2 -0
  72. package/dist/types/tools/impl/process_manager.d.ts.map +1 -1
  73. package/dist/types/tools/impl/shell-command.d.ts.map +1 -1
  74. package/dist/types/tools/impl/shell-sandbox.d.ts +1 -2
  75. package/dist/types/tools/impl/shell-sandbox.d.ts.map +1 -1
  76. package/dist/types/tools/impl/task-stop.d.ts.map +1 -1
  77. package/dist/types/tools/impl/task.d.ts.map +1 -1
  78. package/dist/types/tools/impl/tool-return-clamp.d.ts.map +1 -1
  79. package/dist/types/tools/impl/truncation.d.ts +5 -2
  80. package/dist/types/tools/impl/truncation.d.ts.map +1 -1
  81. package/dist/types/tools/impl/worktree-git.d.ts +14 -0
  82. package/dist/types/tools/impl/worktree-git.d.ts.map +1 -1
  83. package/dist/types/tools/manager.d.ts +4 -13
  84. package/dist/types/tools/manager.d.ts.map +1 -1
  85. package/dist/types/tools/tool-definitions.d.ts +0 -21
  86. package/dist/types/tools/tool-definitions.d.ts.map +1 -1
  87. package/dist/types/tools/tool-permissions.d.ts.map +1 -1
  88. package/dist/types/tools/toolset-catalog.d.ts +13 -0
  89. package/dist/types/tools/toolset-catalog.d.ts.map +1 -0
  90. package/dist/types/tools/toolset.d.ts +10 -27
  91. package/dist/types/tools/toolset.d.ts.map +1 -1
  92. package/dist/types/types/protocol_v2.d.ts +2 -2
  93. package/dist/types/types/protocol_v2.d.ts.map +1 -1
  94. package/dist/types/types/provider-oauth-config.d.ts +24 -0
  95. package/dist/types/types/provider-oauth-config.d.ts.map +1 -0
  96. package/dist/types/types/toolset-protocol.d.ts +1 -1
  97. package/dist/types/types/toolset-protocol.d.ts.map +1 -1
  98. package/dist/types/utils/frontmatter.d.ts.map +1 -1
  99. package/dist/types/websocket/listener/listener-constants.d.ts.map +1 -1
  100. package/docs/examples/mods/memory-citations.ts +0 -2
  101. package/letta.js +2 -2
  102. package/package.json +2 -2
  103. package/scripts/build-pypi.py +172 -0
  104. package/scripts/isolated-unit-tests.json +15 -5
  105. package/scripts/smoke-pypi.py +236 -0
  106. package/scripts/source-file-size-baseline.json +14 -14
  107. package/scripts/stage-pypi-deps.mjs +90 -0
  108. package/skills/creating-mods/references/plan-mode.md +3 -9
  109. package/skills/finding-agents/SKILL.md +6 -2
  110. package/skills/letta-guide/SKILL.md +21 -2
  111. package/skills/self-configuration/SKILL.md +1 -1
  112. package/skills/syncing-memory-filesystem/SKILL.md +2 -3
  113. package/vendor/ink-text-input/build/index.js +10 -2
  114. package/dist/types/tools/impl/glob-gemini.d.ts +0 -16
  115. package/dist/types/tools/impl/glob-gemini.d.ts.map +0 -1
  116. package/dist/types/tools/impl/list-directory-gemini.d.ts +0 -17
  117. package/dist/types/tools/impl/list-directory-gemini.d.ts.map +0 -1
  118. package/dist/types/tools/impl/read-file-gemini.d.ts +0 -14
  119. package/dist/types/tools/impl/read-file-gemini.d.ts.map +0 -1
  120. package/dist/types/tools/impl/read-many-files-gemini.d.ts +0 -19
  121. package/dist/types/tools/impl/read-many-files-gemini.d.ts.map +0 -1
  122. package/dist/types/tools/impl/replace-gemini.d.ts +0 -15
  123. package/dist/types/tools/impl/replace-gemini.d.ts.map +0 -1
  124. package/dist/types/tools/impl/run-shell-command-gemini.d.ts +0 -18
  125. package/dist/types/tools/impl/run-shell-command-gemini.d.ts.map +0 -1
  126. package/dist/types/tools/impl/search-file-content-gemini.d.ts +0 -14
  127. package/dist/types/tools/impl/search-file-content-gemini.d.ts.map +0 -1
  128. package/dist/types/tools/impl/write-file-gemini.d.ts +0 -13
  129. package/dist/types/tools/impl/write-file-gemini.d.ts.map +0 -1
  130. package/dist/types/tools/impl/write-todos-gemini.d.ts +0 -16
  131. package/dist/types/tools/impl/write-todos-gemini.d.ts.map +0 -1
  132. package/dist/types/tools/letta-toolset.d.ts +0 -10
  133. package/dist/types/tools/letta-toolset.d.ts.map +0 -1
  134. package/dist/types/tools/toolset-defaults.d.ts +0 -9
  135. package/dist/types/tools/toolset-defaults.d.ts.map +0 -1
  136. package/dist/types/tools/toolset-options.d.ts +0 -4
  137. package/dist/types/tools/toolset-options.d.ts.map +0 -1
@@ -0,0 +1,172 @@
1
+ """Build a self-contained letta wheel on its target OS/CPU.
2
+
3
+ Prerequisites: locked `bun install --frozen-lockfile`, Bun, npm, Python 3.9+,
4
+ C/C++ toolchain (node-pty). Run from any directory; artifacts go to python/dist.
5
+ Linux release builds MUST run in the manylinux_2_28 CI image, not a newer host.
6
+ No upload/publish is performed by this script.
7
+ """
8
+
9
+ import hashlib
10
+ import importlib.util
11
+ import json
12
+ import os
13
+ import platform
14
+ import re
15
+ import shutil
16
+ import subprocess
17
+ import tarfile
18
+ import tempfile
19
+ import urllib.request
20
+ import zipfile
21
+ from pathlib import Path
22
+
23
+ ROOT = Path(__file__).resolve().parents[1]
24
+ NODE = "22.19.0"
25
+ # Official nodejs.org SHASUMS256.txt; pin the archive, not only the version.
26
+ TARGETS = {
27
+ ("Linux", "x86_64"): (
28
+ "linux-x64",
29
+ "manylinux_2_28_x86_64",
30
+ "d36e56998220085782c0ca965f9d51b7726335aed2f5fc7321c6c0ad233aa96d",
31
+ ),
32
+ ("Linux", "aarch64"): (
33
+ "linux-arm64",
34
+ "manylinux_2_28_aarch64",
35
+ "d32817b937219b8f131a28546035183d79e7fd17a86e38ccb8772901a7cd9009",
36
+ ),
37
+ ("Darwin", "arm64"): (
38
+ "darwin-arm64",
39
+ "macosx_14_0_arm64",
40
+ "c59006db713c770d6ec63ae16cb3edc11f49ee093b5c415d667bb4f436c6526d",
41
+ ),
42
+ ("Darwin", "x86_64"): (
43
+ "darwin-x64",
44
+ "macosx_14_0_x86_64",
45
+ "3cfed4795cd97277559763c5f56e711852d2cc2420bda1cea30c8aa9ac77ce0c",
46
+ ),
47
+ ("Windows", "AMD64"): (
48
+ "win-x64",
49
+ "win_amd64",
50
+ "ea3fad0e67a991d8477d8c01344b56e69c676ccb733f065b22436994b1253f86",
51
+ ),
52
+ }
53
+
54
+
55
+ def run(args, **kwargs):
56
+ # npm.cmd requires a shell on Windows; all arguments here are build-owned.
57
+ if os.name == "nt" and args[0] in ("npm", "bun"):
58
+ args[0] += ".cmd" if args[0] == "npm" else ".exe"
59
+ return subprocess.check_output(args, cwd=ROOT, **kwargs)
60
+
61
+
62
+ def main():
63
+ key = (platform.system(), platform.machine())
64
+ if key not in TARGETS:
65
+ raise SystemExit(f"Unsupported wheel platform: {key[0]} {key[1]}")
66
+ target, wheel_platform, checksum = TARGETS[key]
67
+ if platform.system() == "Linux" and platform.libc_ver()[0] != "glibc":
68
+ raise SystemExit("Only glibc Linux is supported (not musl/Alpine)")
69
+ # Do not falsely label artifacts built on a newer glibc as manylinux_2_28.
70
+ if platform.system() == "Linux" and platform.libc_ver()[1] != "2.28":
71
+ if os.environ.get("LETTA_PYPI_LOCAL_TEST") != "1":
72
+ raise SystemExit(
73
+ "Use the manylinux_2_28 CI image, or LETTA_PYPI_LOCAL_TEST=1 for a non-publishable local wheel"
74
+ )
75
+ wheel_platform = "linux_" + platform.machine()
76
+ version = json.loads((ROOT / "package.json").read_text())["version"]
77
+ if not re.fullmatch(r"\d+\.\d+\.\d+", version):
78
+ raise SystemExit("PyPI builds currently require a stable X.Y.Z package version")
79
+ payload = ROOT / "python/letta_code/_payload"
80
+ shutil.rmtree(payload, ignore_errors=True)
81
+ (payload / "bin").mkdir(parents=True)
82
+ app = payload / "app"
83
+ app.mkdir()
84
+ extension = "zip" if os.name == "nt" else "tar.gz"
85
+ archive_name = f"node-v{NODE}-{target}.{extension}"
86
+ with tempfile.TemporaryDirectory() as temporary:
87
+ archive = Path(temporary) / archive_name
88
+ urllib.request.urlretrieve(
89
+ f"https://nodejs.org/dist/v{NODE}/{archive_name}", archive
90
+ )
91
+ if hashlib.sha256(archive.read_bytes()).hexdigest() != checksum:
92
+ raise SystemExit("Node archive checksum mismatch")
93
+ prefix = f"node-v{NODE}-{target}"
94
+ # Extract only trusted, explicitly named runtime/license files, never npm.
95
+ names = {
96
+ "node.exe" if os.name == "nt" else "bin/node": "bin/node.exe"
97
+ if os.name == "nt"
98
+ else "bin/node",
99
+ "LICENSE": "NODE-LICENSE",
100
+ }
101
+ if extension == "zip":
102
+ with zipfile.ZipFile(archive) as source:
103
+ for member, destination in names.items():
104
+ (payload / destination).write_bytes(
105
+ source.read(f"{prefix}/{member}")
106
+ )
107
+ else:
108
+ with tarfile.open(archive) as source:
109
+ for member, destination in names.items():
110
+ (payload / destination).write_bytes(
111
+ source.extractfile(f"{prefix}/{member}").read()
112
+ )
113
+ node = payload / "bin" / ("node.exe" if os.name == "nt" else "node")
114
+ node.chmod(0o755)
115
+ env = os.environ.copy()
116
+ env["PATH"] = str(node.parent) + os.pathsep + env.get("PATH", "")
117
+ run(["bun", "run", "build"], env=env)
118
+ packed = json.loads(
119
+ run(["npm", "pack", "--dry-run", "--json", "--ignore-scripts"], env=env)
120
+ )
121
+ for entry in packed[0]["files"]:
122
+ path = entry["path"]
123
+ destination = app / path
124
+ destination.parent.mkdir(parents=True, exist_ok=True)
125
+ shutil.copy2(ROOT / path, destination)
126
+ run([str(node), "scripts/stage-pypi-deps.mjs", str(app)], env=env)
127
+ # Native addons must be built with the Node ABI we ship, not Bun/host Node.
128
+ npm = shutil.which("npm")
129
+ native_env = env.copy()
130
+ native_env["npm_config_build_from_source"] = "true"
131
+ subprocess.run(
132
+ [npm, "rebuild", "node-pty", "--foreground-scripts"],
133
+ cwd=app,
134
+ env=native_env,
135
+ check=True,
136
+ shell=os.name == "nt",
137
+ )
138
+ subprocess.run(
139
+ [npm, "rebuild", "@vscode/ripgrep", "--foreground-scripts"],
140
+ cwd=app,
141
+ env=env,
142
+ check=True,
143
+ shell=os.name == "nt",
144
+ )
145
+ pty = app / "node_modules/node-pty"
146
+ shutil.rmtree(pty / "prebuilds", ignore_errors=True)
147
+ for path in ("third_party", "deps", "src", "build/Release/obj.target"):
148
+ shutil.rmtree(pty / path, ignore_errors=True)
149
+ for helper in app.rglob("spawn-helper"):
150
+ helper.chmod(0o755)
151
+ (payload / "manifest.json").write_text(
152
+ json.dumps(
153
+ {
154
+ "version": version,
155
+ "node": NODE,
156
+ "target": target,
157
+ "wheel_platform": wheel_platform,
158
+ },
159
+ indent=2,
160
+ )
161
+ + "\n"
162
+ )
163
+ spec = importlib.util.spec_from_file_location(
164
+ "wheel_backend", ROOT / "python/wheel_backend.py"
165
+ )
166
+ backend = importlib.util.module_from_spec(spec)
167
+ spec.loader.exec_module(backend)
168
+ print(backend.build_wheel(str(ROOT / "python/dist")))
169
+
170
+
171
+ if __name__ == "__main__":
172
+ main()
@@ -1,5 +1,15 @@
1
1
  {
2
2
  "tests": [
3
+ {
4
+ "path": "src/tools/impl/task-fork-receipt.test.ts",
5
+ "timeoutMs": 15000,
6
+ "reason": "Replaces billing I/O and the child process while exercising real fork launch state and task receipts with process-global backend, settings, and subagent state."
7
+ },
8
+ {
9
+ "path": "src/tools/impl/task-computer-routing.test.ts",
10
+ "timeoutMs": 15000,
11
+ "reason": "Mocks subagent discovery and the child-process launch boundary while exercising task() with process-global backend and subagent state."
12
+ },
3
13
  {
4
14
  "path": "src/websocket/listener/turn-cleanup-shared-memory.e2e.test.ts",
5
15
  "timeoutMs": 30000,
@@ -71,6 +81,11 @@
71
81
  "timeoutMs": 15000,
72
82
  "reason": "Renders Ink while mutating the backend mode, backend instance, provider registry, settings singleton, and model cache."
73
83
  },
84
+ {
85
+ "path": "src/cli/app/use-configuration-handlers.test.tsx",
86
+ "timeoutMs": 15000,
87
+ "reason": "Renders the reasoning picker while replacing the shared runtime model catalog; shared batches can retain prior catalog or Ink state."
88
+ },
74
89
  {
75
90
  "path": "src/cli/message-search-cache-warm.test.ts",
76
91
  "timeoutMs": 15000,
@@ -180,11 +195,6 @@
180
195
  "timeoutMs": 15000,
181
196
  "reason": "Uses a top-level Bun module mock for the backend client."
182
197
  },
183
- {
184
- "path": "src/tools/toolset-memfs-detach.test.ts",
185
- "timeoutMs": 15000,
186
- "reason": "Uses a top-level Bun module mock for the backend client."
187
- },
188
198
  {
189
199
  "path": "src/websocket/listen-client-concurrency.test.ts",
190
200
  "timeoutMs": 30000,
@@ -0,0 +1,236 @@
1
+ """Install and exercise a wheel offline, outside the checkout and without Node/npm on PATH."""
2
+
3
+ import json
4
+ import os
5
+ import subprocess
6
+ import sys
7
+ import tempfile
8
+ import venv
9
+ from pathlib import Path
10
+
11
+ ARGUMENT_PROBE = r"""
12
+ import assert from 'node:assert/strict';
13
+ assert.equal(process.env.LETTA_CODE_DISTRIBUTION, 'pypi');
14
+ assert.deepEqual(process.argv.slice(2), ['space argument', 'unicode-λ', '--literal=$HOME']);
15
+ assert.ok(process.execPath.includes('_payload'));
16
+ process.exit(23); // Verify the Python launcher preserves child exit status.
17
+ """
18
+
19
+ RUNTIME_PROBE = r"""
20
+ import {createRequire as bootstrapRequire} from 'node:module';
21
+ const require = bootstrapRequire(import.meta.url);
22
+ const assert = require('node:assert/strict');
23
+ const {createRequire} = require('node:module');
24
+ const {spawnSync} = require('node:child_process');
25
+ const path = require('node:path');
26
+ const fs = require('node:fs');
27
+ const cli = process.argv[2];
28
+ const app = path.dirname(cli);
29
+ const req = createRequire(cli);
30
+ const env = {...process.env};
31
+ delete env.NODE_OPTIONS;
32
+ assert.equal(process.env.LETTA_CODE_DISTRIBUTION, 'pypi');
33
+ assert.ok(process.execPath.includes('_payload'));
34
+ const child = spawnSync('node', ['-p', 'process.execPath'], {env, encoding:'utf8'});
35
+ assert.equal(child.status, 0, child.stderr);
36
+ assert.equal(fs.realpathSync(child.stdout.trim()), fs.realpathSync(process.execPath));
37
+ if (process.platform !== 'win32') {
38
+ const direct = spawnSync(cli, ['--version'], {env, encoding:'utf8'});
39
+ assert.equal(direct.status, 0, direct.stderr); // Executable bits + /usr/bin/env node.
40
+ assert.match(direct.stdout, /Letta Code/);
41
+ }
42
+ assert.ok(fs.existsSync(path.join(app, 'skills', 'letta-guide', 'SKILL.md')));
43
+ assert.ok(fs.existsSync(path.join(app, 'assets', 'tutor-profile.png')));
44
+ const rg = spawnSync(req('@vscode/ripgrep').rgPath, ['--version'], {env, encoding:'utf8'});
45
+ assert.equal(rg.status, 0, rg.stderr);
46
+ assert.match(rg.stdout, /ripgrep/);
47
+ assert.equal(typeof req('ws').WebSocketServer, 'function');
48
+ (async () => {
49
+ const {Bot} = req('grammy');
50
+ const bot = new Bot('123:test');
51
+ bot.api.config.use(async () => ({ok:true, result:{id:123, is_bot:true, first_name:'wheel', username:'wheel_bot'}}));
52
+ await bot.init();
53
+ const svg = '<svg xmlns="http://www.w3.org/2000/svg" width="3000" height="2"><rect width="3000" height="2" fill="red"/></svg>';
54
+ const resized = spawnSync(process.execPath, [path.join(app, 'image-resize-worker.js'), 'image/svg+xml'], {input:svg, env});
55
+ assert.equal(resized.status, 0, resized.stderr.toString());
56
+ const image = JSON.parse(resized.stdout);
57
+ assert.ok(image.width > 0 && image.width < 3000, JSON.stringify(image));
58
+ const ptyCommand = process.platform === 'win32' ? process.env.ComSpec : process.execPath;
59
+ const ptyArgs = process.platform === 'win32' ? ['/d', '/s', '/c', 'echo LETTA_PTY_OK'] : [cli, '--help'];
60
+ const ptyExpected = process.platform === 'win32' ? /LETTA_PTY_OK/ : /USAGE/;
61
+ const pty = req('node-pty').spawn(ptyCommand, ptyArgs, {env, cols:100, rows:30});
62
+ let output = '';
63
+ pty.onData(data => { output += data; });
64
+ const timeout = setTimeout(() => { pty.kill(); throw new Error('PTY timed out'); }, 30000);
65
+ pty.onExit(({exitCode}) => {
66
+ clearTimeout(timeout);
67
+ assert.equal(exitCode, 0, output);
68
+ assert.match(output, ptyExpected);
69
+ console.log('native PTY, image worker, ripgrep, Telegram, assets and child Node passed');
70
+ process.exit(0);
71
+ });
72
+ })().catch(error => {console.error(error); process.exit(1)});
73
+ // Hold the script until the asynchronous probe exits.
74
+ await new Promise(() => {});
75
+ """
76
+
77
+
78
+ def main():
79
+ wheel = Path(sys.argv[1]).resolve()
80
+ if wheel.stat().st_size >= 100 * 1024 * 1024:
81
+ raise SystemExit("Wheel exceeds the default PyPI 100 MiB artifact limit")
82
+ with tempfile.TemporaryDirectory(prefix="letta wheel smoke ") as temporary:
83
+ root = Path(temporary)
84
+ environment = root / "venv"
85
+ venv.create(environment, with_pip=True)
86
+ binary = environment / ("Scripts" if os.name == "nt" else "bin")
87
+ python = binary / ("python.exe" if os.name == "nt" else "python")
88
+ cli = binary / ("letta.exe" if os.name == "nt" else "letta")
89
+ subprocess.run(
90
+ [
91
+ str(python),
92
+ "-m",
93
+ "pip",
94
+ "install",
95
+ "--no-index",
96
+ "--no-deps",
97
+ str(wheel),
98
+ ],
99
+ check=True,
100
+ )
101
+ home = root / "home"
102
+ home.mkdir()
103
+ env = {
104
+ k: v
105
+ for k, v in os.environ.items()
106
+ if not k.startswith(("LETTA", "NODE_", "NPM_"))
107
+ and k not in ("AGENT_ID", "CONVERSATION_ID", "MEMORY_DIR")
108
+ }
109
+ env.update(
110
+ HOME=str(home),
111
+ USERPROFILE=str(home),
112
+ PATH=str(binary),
113
+ DISABLE_AUTOUPDATER="0",
114
+ )
115
+ if os.name == "nt":
116
+ env["PATH"] += os.pathsep + str(Path(os.environ["SystemRoot"]) / "System32")
117
+ for args, expected, code in [
118
+ (["--help"], "USAGE", 0),
119
+ (["--version"], "Letta Code", 0),
120
+ (["update"], "uv tool upgrade letta", 1),
121
+ ]:
122
+ result = subprocess.run(
123
+ [str(cli), *args],
124
+ cwd=root,
125
+ env=env,
126
+ check=False,
127
+ capture_output=True,
128
+ text=True,
129
+ timeout=45,
130
+ )
131
+ assert result.returncode == code, (args, result.stdout, result.stderr)
132
+ assert expected in result.stdout + result.stderr, result
133
+ env.update(
134
+ LETTA_LOCAL_BACKEND_DIR=str(root / "local-store"),
135
+ LETTA_LOCAL_BACKEND_EXECUTOR="deterministic",
136
+ LETTA_SKIP_KEYCHAIN_CHECK="1",
137
+ )
138
+ result = subprocess.run(
139
+ [
140
+ str(cli),
141
+ "--backend",
142
+ "local",
143
+ "--ephemeral",
144
+ "-m",
145
+ "openai/gpt-5.6-luna",
146
+ "-p",
147
+ "wheel runtime smoke",
148
+ "--tools=",
149
+ "--output-format",
150
+ "json",
151
+ ],
152
+ cwd=root,
153
+ env=env,
154
+ check=False,
155
+ capture_output=True,
156
+ text=True,
157
+ timeout=60,
158
+ )
159
+ assert result.returncode == 0, (result.stdout, result.stderr)
160
+ response = json.loads(result.stdout)
161
+ assert response["conversation_id"].startswith("local-conv-"), response
162
+ assert response["result"], response
163
+ print("Installed headless deterministic local turn passed")
164
+ argument_probe = root / "argument-probe.mjs"
165
+ argument_probe.write_text(ARGUMENT_PROBE, encoding="utf-8")
166
+ env["NODE_OPTIONS"] = "--import=" + json.dumps(argument_probe.as_uri())
167
+ result = subprocess.run(
168
+ [str(cli), "space argument", "unicode-λ", "--literal=$HOME"],
169
+ cwd=root,
170
+ env=env,
171
+ check=False,
172
+ timeout=60,
173
+ )
174
+ assert result.returncode == 23, result
175
+ del env["NODE_OPTIONS"]
176
+ payload = Path(
177
+ subprocess.check_output(
178
+ [
179
+ str(python),
180
+ "-c",
181
+ "from pathlib import Path; import letta_code; print(Path(letta_code.__file__).parent / '_payload')",
182
+ ],
183
+ text=True,
184
+ ).strip()
185
+ )
186
+ node = payload / "bin" / ("node.exe" if os.name == "nt" else "node")
187
+ installed_cli = payload / "app" / "letta.js"
188
+ runtime_probe = root / "runtime-probe.mjs"
189
+ runtime_probe.write_text(RUNTIME_PROBE, encoding="utf-8")
190
+ env["PATH"] = str(node.parent) + os.pathsep + env["PATH"]
191
+ env["LETTA_CODE_DISTRIBUTION"] = "pypi"
192
+ result = subprocess.run(
193
+ [str(node), str(runtime_probe), str(installed_cli)],
194
+ cwd=root,
195
+ env=env,
196
+ check=False,
197
+ timeout=60,
198
+ )
199
+ assert result.returncode == 0, result
200
+ if os.name != "nt":
201
+ import selectors
202
+ import signal
203
+
204
+ signal_probe = root / "signal.mjs"
205
+ signal_probe.write_text(
206
+ "process.on('SIGTERM', () => process.exit(42)); console.log(process.pid); setInterval(() => {}, 1000); await new Promise(() => {});"
207
+ )
208
+ env["NODE_OPTIONS"] = "--import=" + json.dumps(signal_probe.as_uri())
209
+ process = subprocess.Popen(
210
+ [str(cli)],
211
+ cwd=root,
212
+ env=env,
213
+ stdout=subprocess.PIPE,
214
+ stderr=subprocess.PIPE,
215
+ text=True,
216
+ )
217
+ try:
218
+ with selectors.DefaultSelector() as selector:
219
+ selector.register(process.stdout, selectors.EVENT_READ)
220
+ assert selector.select(timeout=15), "Signal probe did not start"
221
+ assert int(process.stdout.readline()) == process.pid, (
222
+ "Launcher did not exec Node"
223
+ )
224
+ process.send_signal(signal.SIGTERM)
225
+ process.communicate(timeout=15)
226
+ assert process.returncode == 42, process.returncode
227
+ finally:
228
+ if process.poll() is None:
229
+ process.kill()
230
+ process.wait()
231
+ print("POSIX PID and SIGTERM forwarding passed")
232
+ print(f"Installed wheel smoke passed: {wheel.name}")
233
+
234
+
235
+ if __name__ == "__main__":
236
+ main()
@@ -1,29 +1,29 @@
1
1
  {
2
2
  "src/agent/client-skills.test.ts": 1134,
3
3
  "src/agent/memory-git.ts": 2097,
4
- "src/backend/local-backend.test.ts": 2532,
5
- "src/backend/local/local-backend.ts": 1012,
6
- "src/backend/local/local-store.ts": 3459,
7
- "src/backend/pi-stream-adapter.test.ts": 1304,
8
- "src/cli/app/AppCoordinator.tsx": 5141,
4
+ "src/backend/local-backend.test.ts": 2473,
5
+ "src/backend/local/local-backend.ts": 1005,
6
+ "src/backend/local/local-store.ts": 3457,
7
+ "src/backend/pi-stream-adapter.test.ts": 1255,
8
+ "src/cli/app/AppCoordinator.tsx": 5138,
9
9
  "src/cli/app/AppView.tsx": 1735,
10
10
  "src/cli/app/use-approval-flow.ts": 1163,
11
- "src/cli/app/use-configuration-handlers.ts": 1421,
11
+ "src/cli/app/use-configuration-handlers.ts": 1418,
12
12
  "src/cli/app/use-conversation-loop.ts": 2903,
13
- "src/cli/app/use-submit-handler.ts": 3979,
13
+ "src/cli/app/use-submit-handler.ts": 3804,
14
14
  "src/cli/components/AgentSelector.tsx": 1104,
15
15
  "src/cli/components/InputRich.tsx": 2225,
16
16
  "src/cli/components/ModelSelector.tsx": 1259,
17
- "src/cli/components/ProviderSelector.tsx": 1692,
17
+ "src/cli/components/ProviderSelector.tsx": 1564,
18
18
  "src/cli/components/ToolCallMessageRich.tsx": 1013,
19
19
  "src/cli/helpers/accumulator.ts": 1592,
20
20
  "src/cli/helpers/reflection-transcript.ts": 1926,
21
21
  "src/cli/mods/local-mod-loader.test.ts": 1043,
22
22
  "src/cli/reflection-transcript.test.ts": 1084,
23
23
  "src/cli/subcommands/skills.ts": 1264,
24
- "src/headless.ts": 4824,
24
+ "src/headless.ts": 4808,
25
25
  "src/hooks/integration.test.ts": 1147,
26
- "src/index.ts": 2612,
26
+ "src/index.ts": 2606,
27
27
  "src/mods/learning-harness.ts": 2434,
28
28
  "src/mods/mod-engine.test.ts": 2153,
29
29
  "src/mods/mod-engine.ts": 1838,
@@ -34,14 +34,14 @@
34
34
  "src/permissions/read-only-shell.ts": 1969,
35
35
  "src/providers/chatgpt-usage-service.ts": 1112,
36
36
  "src/settings-manager.test.ts": 1635,
37
- "src/settings-manager.ts": 2102,
38
- "src/tools/manager.ts": 2904,
39
- "src/tools/tool-execution-context.test.ts": 1136,
37
+ "src/settings-manager.ts": 2101,
38
+ "src/tools/manager.ts": 2798,
39
+ "src/tools/tool-execution-context.test.ts": 1114,
40
40
  "src/types/protocol_v2.ts": 2593,
41
41
  "src/websocket/listen-client-concurrency.test.ts": 2666,
42
42
  "src/websocket/listen-client-protocol.test.ts": 5762,
43
43
  "src/websocket/listener/commands/memory.ts": 1114,
44
44
  "src/websocket/listener/file-commands.ts": 1053,
45
- "src/websocket/listener/protocol-inbound.ts": 2137,
45
+ "src/websocket/listener/protocol-inbound.ts": 2126,
46
46
  "src/websocket/listener/protocol-outbound.ts": 1022
47
47
  }
@@ -0,0 +1,90 @@
1
+ // Copy the locked runtime dependency closure, preserving Node's nested versions.
2
+ // No dependency installation happens on the user's machine.
3
+ import {
4
+ cpSync,
5
+ existsSync,
6
+ mkdirSync,
7
+ readdirSync,
8
+ readFileSync,
9
+ realpathSync,
10
+ } from "node:fs";
11
+ import { dirname, join, resolve } from "node:path";
12
+
13
+ const root = process.cwd();
14
+ const app = resolve(process.argv[2]);
15
+ // sharp-electron is only reachable under Electron, never our bundled Node.
16
+ const roots = ["ws", "@vscode/ripgrep", "node-pty", "grammy"];
17
+
18
+ function locate(name, from) {
19
+ for (let dir = from; ; dir = dirname(dir)) {
20
+ const candidate = join(dir, "node_modules", name);
21
+ if (existsSync(join(candidate, "package.json")))
22
+ return realpathSync(candidate);
23
+ if (dir === dirname(dir))
24
+ throw new Error(`Missing locked dependency ${name} from ${from}`);
25
+ }
26
+ }
27
+ function compatible(pkg) {
28
+ return (
29
+ (!pkg.os || pkg.os.includes(process.platform)) &&
30
+ (!pkg.cpu || pkg.cpu.includes(process.arch)) &&
31
+ (!pkg.libc || pkg.libc.includes("glibc"))
32
+ );
33
+ }
34
+ function copy(name, from, dest, ancestors = new Set()) {
35
+ const source = locate(name, from);
36
+ const pkg = JSON.parse(readFileSync(join(source, "package.json")));
37
+ if (!compatible(pkg)) return;
38
+ if (ancestors.has(source)) throw new Error(`Dependency cycle at ${source}`);
39
+ const target = join(dest, "node_modules", name);
40
+ mkdirSync(dirname(target), { recursive: true });
41
+ cpSync(source, target, {
42
+ recursive: true,
43
+ dereference: true,
44
+ filter: (path) =>
45
+ path === source ||
46
+ !["node_modules", ".git"].includes(
47
+ path.slice(source.length + 1).split(/[\\/]/)[0],
48
+ ),
49
+ });
50
+ const chain = new Set([...ancestors, source]);
51
+ const optional = pkg.optionalDependencies || {};
52
+ for (const dependency of Object.keys({ ...pkg.dependencies, ...optional })) {
53
+ try {
54
+ locate(dependency, source);
55
+ } catch (error) {
56
+ if (dependency in optional) continue;
57
+ throw error;
58
+ }
59
+ copy(dependency, source, target, chain);
60
+ }
61
+ }
62
+ for (const name of roots) copy(name, root, app);
63
+ // Bun bundles Sharp JS but leaves its computed @img native requires unresolved.
64
+ const sharp = locate("sharp", root);
65
+ const sharpPackage = JSON.parse(readFileSync(join(sharp, "package.json")));
66
+ for (const name of Object.keys(sharpPackage.optionalDependencies || {})) {
67
+ try {
68
+ locate(name, sharp);
69
+ } catch {
70
+ continue;
71
+ }
72
+ copy(name, sharp, app);
73
+ }
74
+
75
+ // Retain license notices for code in the JS bundle as well as external modules.
76
+ function licenses(dir, destination) {
77
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
78
+ const path = join(dir, entry.name);
79
+ if (entry.isDirectory() && entry.name !== ".bin")
80
+ licenses(path, join(destination, entry.name));
81
+ else if (
82
+ entry.isFile() &&
83
+ /^(licen[sc]e|copying|notice)([.-]|$)/i.test(entry.name)
84
+ ) {
85
+ mkdirSync(destination, { recursive: true });
86
+ cpSync(path, join(destination, entry.name));
87
+ }
88
+ }
89
+ }
90
+ licenses(join(root, "node_modules"), join(app, "third-party-licenses"));
@@ -109,7 +109,7 @@ In plan mode, you should:
109
109
  Remember: DO NOT write or edit any files except the plan file. This is a read-only exploration and planning phase.
110
110
 
111
111
  Plan file path: ${session.planFilePath}
112
- If using apply_patch, use this exact relative patch path: ${relativePatchPath}`;
112
+ If using ApplyPatch, use this exact relative patch path: ${relativePatchPath}`;
113
113
  }
114
114
 
115
115
  export default function activate(letta) {
@@ -161,7 +161,7 @@ function buildActiveReminder(session, cwd) {
161
161
  Plan mode is active. The user indicated that they do not want you to execute yet -- you MUST NOT make any edits (with the exception of the plan file mentioned below), run any non-readonly tools (including changing configs or making commits), or otherwise make any changes to the system. This supersedes any other instructions you have received. Instead, you should:
162
162
  1. Answer the user's query comprehensively, using the AskUserQuestion tool if you need to ask the user clarifying questions.
163
163
  2. Write your implementation plan to the plan file. Plan file path: ${session.planFilePath}
164
- 3. If using apply_patch, use this exact relative path in patch headers: ${relativePatchPath}
164
+ 3. If using ApplyPatch, use this exact relative path in patch headers: ${relativePatchPath}
165
165
  4. Use direct read-only tools for exploration. Do not launch coding, general-purpose, or fork subagents in plan mode; they may mutate files and should be denied. Only recall-style subagents are allowed if available.
166
166
  5. When the plan is complete, read the plan file and present the full current plan text to the user with AskUserQuestion. The question body must include the entire plan, not a summary. The question should offer at least "Approve" and "Revise" options.
167
167
  6. If the user approves, call exit_plan_mode immediately. If the user asks to revise, stay in plan mode, update the plan file, then read and present the full revised plan again.
@@ -180,26 +180,21 @@ if (letta.capabilities.events.turns) {
180
180
 
181
181
  ## Permission overlay
182
182
 
183
- Use a permission overlay, not `tool_start`, for policy. Normalize tool names by family; UI display names and provider-specific tool names drift (`Read`, `read`, `read_file`, `ReadFile`, `SearchFileContent`, etc.). Keep pure read-only tools separate from planning coordination tools like `AskUserQuestion` and todo/plan updates so the policy stays honest.
183
+ Use a permission overlay, not `tool_start`, for policy. Normalize tool names by family; UI display names and provider-specific tool names drift (`Read`, `read`, `read_file`, `ReadFile`, etc.). Keep pure read-only tools separate from planning coordination tools like `AskUserQuestion` and todo/plan updates so the policy stays honest.
184
184
 
185
185
  ```ts
186
186
  const readOnlyToolNames = new Set([
187
187
  "glob",
188
- "globgemini",
189
188
  "grep",
190
189
  "grepfiles",
191
190
  "list",
192
191
  "listdir",
193
- "listdirectory",
194
192
  "ls",
195
193
  "notebookread",
196
194
  "read",
197
195
  "readfile",
198
- "readfilegemini",
199
196
  "readlsp",
200
- "readmanyfiles",
201
197
  "search",
202
- "searchfilecontent",
203
198
  "searchfiles",
204
199
  "skill",
205
200
  "taskoutput",
@@ -212,7 +207,6 @@ const planningToolNames = new Set([
212
207
  "exitplanmode",
213
208
  "todowrite",
214
209
  "updateplan",
215
- "writetodos",
216
210
  ]);
217
211
 
218
212
  const readOnlySubagentTypes = new Set(["recall"]);