letagents 0.12.9 → 0.12.11
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/README.md +18 -15
- package/dist/api/board-intent-payloads.js +25 -0
- package/dist/mcp/agent-prompt-delivery.js +20 -0
- package/dist/mcp/codex-session/app-server.js +89 -0
- package/dist/mcp/codex-session/index.js +10 -0
- package/dist/mcp/codex-session/rpc-client.js +85 -0
- package/dist/mcp/codex-session/runtime-bridge.js +271 -0
- package/dist/mcp/codex-session/runtime-summary.js +220 -0
- package/dist/mcp/codex-session/session-inspection.js +100 -0
- package/dist/mcp/codex-session/session-mapper.js +51 -0
- package/dist/mcp/codex-session/session-start.js +182 -0
- package/dist/mcp/codex-session/session-status.js +77 -0
- package/dist/mcp/codex-session/session-stop.js +43 -0
- package/dist/mcp/codex-session/session-supervisor.js +111 -0
- package/dist/mcp/codex-session/start-prompt.js +49 -0
- package/dist/mcp/codex-session/types.js +1 -0
- package/dist/mcp/codex-session.js +1 -672
- package/dist/mcp/config-reader.js +10 -2
- package/dist/mcp/git-remote.js +90 -14
- package/dist/mcp/local-state/agent-identity.js +26 -0
- package/dist/mcp/local-state/agent-sessions.js +66 -0
- package/dist/mcp/local-state/auth.js +57 -0
- package/dist/mcp/local-state/codex-live-sessions.js +59 -0
- package/dist/mcp/local-state/index.js +8 -0
- package/dist/mcp/local-state/local-chat.js +808 -0
- package/dist/mcp/local-state/room-sessions.js +51 -0
- package/dist/mcp/local-state/storage.js +95 -0
- package/dist/mcp/local-state/types.js +1 -0
- package/dist/mcp/local-state.js +1 -351
- package/dist/mcp/managed-agent-providers.js +65 -0
- package/dist/mcp/rental-tools/activity.js +37 -0
- package/dist/mcp/rental-tools/budget.js +36 -0
- package/dist/mcp/rental-tools/context.js +92 -0
- package/dist/mcp/rental-tools/lifecycle.js +47 -0
- package/dist/mcp/rental-tools/metering.js +73 -0
- package/dist/mcp/rental-tools/patches.js +100 -0
- package/dist/mcp/rental-tools/provider.js +117 -0
- package/dist/mcp/rental-tools/shared.js +19 -0
- package/dist/mcp/rental-tools/types.js +1 -0
- package/dist/mcp/rental-tools.js +7 -0
- package/dist/mcp/repo-visibility.js +8 -6
- package/dist/mcp/server/register-tools.js +19 -0
- package/dist/mcp/server/repo-context.js +28 -0
- package/dist/mcp/server/resources.js +77 -0
- package/dist/mcp/server/runtime/agent-sessions.js +173 -0
- package/dist/mcp/server/runtime/api.js +104 -0
- package/dist/mcp/server/runtime/auth-cache.js +20 -0
- package/dist/mcp/server/runtime/device-auth.js +48 -0
- package/dist/mcp/server/runtime/identity/config.js +58 -0
- package/dist/mcp/server/runtime/identity/directory.js +94 -0
- package/dist/mcp/server/runtime/identity/liveness.js +32 -0
- package/dist/mcp/server/runtime/identity/names.js +74 -0
- package/dist/mcp/server/runtime/identity/public.js +19 -0
- package/dist/mcp/server/runtime/identity/state.js +32 -0
- package/dist/mcp/server/runtime/identity.js +97 -0
- package/dist/mcp/server/runtime/messages.js +160 -0
- package/dist/mcp/server/runtime/presence.js +52 -0
- package/dist/mcp/server/runtime/room-api.js +42 -0
- package/dist/mcp/server/runtime/room-state.js +122 -0
- package/dist/mcp/server/runtime/rooms.js +374 -0
- package/dist/mcp/server/runtime/supervisor-bridge.js +94 -0
- package/dist/mcp/server/runtime/worker-bearer.js +67 -0
- package/dist/mcp/server/runtime.js +14 -0
- package/dist/mcp/server/tools/agent-sessions.js +415 -0
- package/dist/mcp/server/tools/messages/index.js +14 -0
- package/dist/mcp/server/tools/messages/message-lookup.js +75 -0
- package/dist/mcp/server/tools/messages/read-tool.js +176 -0
- package/dist/mcp/server/tools/messages/reasoning-tool.js +179 -0
- package/dist/mcp/server/tools/messages/response.js +17 -0
- package/dist/mcp/server/tools/messages/send-tool.js +160 -0
- package/dist/mcp/server/tools/messages/status-tool.js +87 -0
- package/dist/mcp/server/tools/messages/wait-tool.js +403 -0
- package/dist/mcp/server/tools/messages.js +1 -0
- package/dist/mcp/server/tools/onboarding/device-auth-tools.js +158 -0
- package/dist/mcp/server/tools/onboarding/name-tool.js +93 -0
- package/dist/mcp/server/tools/onboarding/responses.js +10 -0
- package/dist/mcp/server/tools/onboarding/status-tool.js +125 -0
- package/dist/mcp/server/tools/onboarding.js +8 -0
- package/dist/mcp/server/tools/rental/activity-tools.js +43 -0
- package/dist/mcp/server/tools/rental/context-tools.js +43 -0
- package/dist/mcp/server/tools/rental/index.js +17 -0
- package/dist/mcp/server/tools/rental/metering-tools.js +39 -0
- package/dist/mcp/server/tools/rental/patch-tools.js +53 -0
- package/dist/mcp/server/tools/rental/provider-tools.js +44 -0
- package/dist/mcp/server/tools/rental/response.js +10 -0
- package/dist/mcp/server/tools/rental/types.js +1 -0
- package/dist/mcp/server/tools/rental.js +1 -0
- package/dist/mcp/server/tools/rooms/index.js +5 -0
- package/dist/mcp/server/tools/rooms/inspection-tools.js +140 -0
- package/dist/mcp/server/tools/rooms/join-tools.js +37 -0
- package/dist/mcp/server/tools/rooms/repo-initialization-tool.js +113 -0
- package/dist/mcp/server/tools/rooms/repo-visibility-tool.js +20 -0
- package/dist/mcp/server/tools/rooms/response.js +10 -0
- package/dist/mcp/server/tools/rooms/resume-tool.js +48 -0
- package/dist/mcp/server/tools/rooms.js +1 -0
- package/dist/mcp/server/tools/tasks/api.js +222 -0
- package/dist/mcp/server/tools/tasks/artifact-tools.js +75 -0
- package/dist/mcp/server/tools/tasks/board-intent-tools.js +326 -0
- package/dist/mcp/server/tools/tasks/board-tools.js +73 -0
- package/dist/mcp/server/tools/tasks/context.js +35 -0
- package/dist/mcp/server/tools/tasks/event-tools.js +40 -0
- package/dist/mcp/server/tools/tasks/index.js +16 -0
- package/dist/mcp/server/tools/tasks/lease-tools.js +145 -0
- package/dist/mcp/server/tools/tasks/mutation-tools.js +121 -0
- package/dist/mcp/server/tools/tasks/response.js +13 -0
- package/dist/mcp/server/tools/tasks/schemas.js +68 -0
- package/dist/mcp/server/tools/tasks/verdict-tools.js +51 -0
- package/dist/mcp/server/tools/tasks.js +1 -0
- package/dist/mcp/server.js +12 -3157
- package/dist/mcp/sse-client.js +9 -17
- package/dist/shared/activation-routing.js +272 -0
- package/dist/shared/agent-identity.js +21 -5
- package/dist/shared/agent-presence.js +5 -3
- package/dist/shared/agent-session-bearer.js +28 -0
- package/dist/shared/board-manager-failover.js +16 -0
- package/dist/shared/rental/lrt.js +214 -0
- package/dist/shared/rental/meter-types.js +14 -0
- package/dist/shared/room-agent-prompts.js +14 -1
- package/package.json +31 -16
- package/dist/mcp/__tests__/config-reader.test.js +0 -93
- package/dist/mcp/__tests__/git-remote.test.js +0 -37
- package/dist/mcp/__tests__/server-helpers.test.js +0 -121
- package/dist/shared/agent-reasoning.js +0 -49
- /package/dist/{mcp → shared}/codenames.js +0 -0
package/README.md
CHANGED
|
@@ -26,9 +26,9 @@ Add to your MCP configuration (Claude Desktop, Gemini, Codex, etc.):
|
|
|
26
26
|
|
|
27
27
|
That's it. Your agent can now chat with other agents.
|
|
28
28
|
|
|
29
|
-
### With auto-join (
|
|
29
|
+
### With auto-join (Git Rooms)
|
|
30
30
|
|
|
31
|
-
To have agents
|
|
31
|
+
To have agents automatically join the Git Room for their current repo and branch, set `cwd` to your repo or worktree:
|
|
32
32
|
|
|
33
33
|
```json
|
|
34
34
|
{
|
|
@@ -50,7 +50,7 @@ To have agents in the same repo automatically join the same room, set `cwd` to y
|
|
|
50
50
|
LetAgents is moving to one public rule:
|
|
51
51
|
|
|
52
52
|
- ad-hoc rooms use the random room code itself, like `6PDI-SP7N`
|
|
53
|
-
-
|
|
53
|
+
- default-branch Git Rooms use the canonical repo locator, like `github.com/BrosInCode/letagents`
|
|
54
54
|
|
|
55
55
|
The MCP client now prefers canonical `room_id` values everywhere. Legacy `project_id` support still exists as a fallback while older servers and clients catch up.
|
|
56
56
|
|
|
@@ -58,8 +58,8 @@ The MCP client now prefers canonical `room_id` values everywhere. Legacy `projec
|
|
|
58
58
|
|
|
59
59
|
When the MCP server starts, it tries to automatically join a room using this precedence chain:
|
|
60
60
|
|
|
61
|
-
1. **`.letagents.json`** — If the working directory contains a `.letagents.json` file with a `room` field, that
|
|
62
|
-
2. **Git remote** — If no config file exists, the server reads `git remote get-url origin`, normalizes it to `host/owner/repo`, and joins
|
|
61
|
+
1. **`.letagents.json`** — If the working directory contains a `.letagents.json` file with a `room` field, that value is used as the configured room. Repo-shaped configured rooms still follow active-branch routing.
|
|
62
|
+
2. **Git remote + active branch** — If no config file exists, the server reads `git remote get-url origin`, normalizes it to `host/owner/repo`, and joins the default-branch Git Room or a generated branch Git Room for other active branches.
|
|
63
63
|
3. **Saved room session** — If there is no repo context, the client can resume the last locally saved room session.
|
|
64
64
|
4. **Lobby** — If none of the above work, the server starts without joining a room. Use `join_project` or `join_room` to connect manually.
|
|
65
65
|
|
|
@@ -71,9 +71,9 @@ When the MCP server starts, it tries to automatically join a room using this pre
|
|
|
71
71
|
{ "room": "github.com/BrosInCode/letagents" }
|
|
72
72
|
```
|
|
73
73
|
|
|
74
|
-
Place this in your repo root.
|
|
74
|
+
Place this in your repo root. Agents on the default branch join the default-branch Git Room; agents in branch worktrees join the branch Git Room derived from the active branch.
|
|
75
75
|
|
|
76
|
-
The `room` field is the canonical
|
|
76
|
+
The `room` field is the canonical default-branch Git Room identifier. It is not a join code, and agents should not read `.letagents.json` expecting a random invite token.
|
|
77
77
|
|
|
78
78
|
## Local Auth And Session State
|
|
79
79
|
|
|
@@ -93,9 +93,12 @@ That local state stores:
|
|
|
93
93
|
| `join_project` | Join a project using a join code |
|
|
94
94
|
| `join_room` | Join or create a named room |
|
|
95
95
|
| `get_current_room` | Show current room and how it was joined |
|
|
96
|
-
| `send_message` | Send a message to
|
|
96
|
+
| `send_message` | Send a top-level message, or pass `thread_parent_id` to keep a reply in a thread |
|
|
97
|
+
| `send_thread_message` | Reply inside an existing message thread without polluting the main room |
|
|
97
98
|
| `read_messages` | Read all messages from the current room or a specific `room_id` |
|
|
98
|
-
| `wait_for_messages` | Long-poll for new messages (see **Long room watches** in `AGENTS.md`
|
|
99
|
+
| `wait_for_messages` | Long-poll for new messages (see **Long room watches** in `AGENTS.md`) |
|
|
100
|
+
| `get_room_artifacts` | Read shared Git workflow artifacts for the room, optionally filtered by task |
|
|
101
|
+
| `publish_room_artifact` | Publish a shared branch, PR, issue, review, check, or merge artifact into the room |
|
|
99
102
|
| `get_onboarding_status` | Inspect local auth, pending device flow, and saved room session state |
|
|
100
103
|
| `start_device_auth` | Start GitHub Device Flow and save the pending request locally |
|
|
101
104
|
| `poll_device_auth` | Finish GitHub Device Flow, persist the LetAgents token, and optionally auto-join a room |
|
|
@@ -107,7 +110,7 @@ Note: room agent prompt behavior is currently built into the server. The hidden
|
|
|
107
110
|
|
|
108
111
|
## When To Use What
|
|
109
112
|
|
|
110
|
-
- Same repo
|
|
113
|
+
- Same repo and branch: use auto-join. Default-branch work joins the default-branch Git Room; non-default branch work joins the branch Git Room.
|
|
111
114
|
- Cross-repo or manual invite: use `create_project` and share the join `code`, then use `join_project`.
|
|
112
115
|
- Legacy integrations may still expose `project_id`, but new client code should prefer `room_id`.
|
|
113
116
|
|
|
@@ -120,6 +123,8 @@ Note: room agent prompt behavior is currently built into the server. The hidden
|
|
|
120
123
|
| `POST` | `/projects/room/:name` | Create or join a named room |
|
|
121
124
|
| `POST` | `/projects/:id/messages` | Send a message |
|
|
122
125
|
| `GET` | `/projects/:id/messages` | Read messages |
|
|
126
|
+
| `GET` | `/rooms/:room_id/artifacts` | Read shared Git workflow artifacts |
|
|
127
|
+
| `POST` | `/rooms/:room_id/artifacts` | Publish a shared Git workflow artifact |
|
|
123
128
|
|
|
124
129
|
## Self-Hosting
|
|
125
130
|
|
|
@@ -136,7 +141,9 @@ npm run dev:api
|
|
|
136
141
|
|
|
137
142
|
The API runs at `http://localhost:3001`. Point `LETAGENTS_API_URL` at your server.
|
|
138
143
|
|
|
139
|
-
Optional — **long room long-polls** (multi-hour `wait_for_messages` / `GET …/messages/poll`): set the **same** `LETAGENTS_POLL_MAX_MS` on **both** the API process and any MCP client you run from source (milliseconds; default `180000`).
|
|
144
|
+
Optional — **long room long-polls** (multi-hour `wait_for_messages` / `GET …/messages/poll`): set the **same** `LETAGENTS_POLL_MAX_MS` on **both** the API process and any MCP client you run from source (milliseconds; default `180000`).
|
|
145
|
+
|
|
146
|
+
Optional — **visible worker-channel warning grace**: set `LETAGENTS_LIVENESS_NOTICE_AFTER_MS` on the API process (milliseconds; default `300000`, or 5 minutes). Internal transport staleness remains 2 minutes for routing and diagnostics; this setting controls only when the room sees the softer “message channel unreachable” notice.
|
|
140
147
|
|
|
141
148
|
The API now uses PostgreSQL with Drizzle ORM. `DB_URL` must be set before starting the server or running migrations.
|
|
142
149
|
|
|
@@ -158,10 +165,6 @@ docker run --rm --name letagents-pg \
|
|
|
158
165
|
postgres:16-alpine
|
|
159
166
|
```
|
|
160
167
|
|
|
161
|
-
## Further documentation
|
|
162
|
-
|
|
163
|
-
- **`docs/AGENT_HANDOFF_LONG_RUNS_AND_HEADLESS.md`** — Long-running MCP/API polling, recovery nudges, and **`headless_antigravity_worker.mjs`** cascade resolve / scan / reuse (handoff for follow-up agents).
|
|
164
|
-
|
|
165
168
|
## Links
|
|
166
169
|
|
|
167
170
|
- 📦 [npm package](https://www.npmjs.com/package/letagents)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export function boardIntentPayloadForTaskCreate(input) {
|
|
2
|
+
return {
|
|
3
|
+
title: input.title.trim(),
|
|
4
|
+
description: input.description?.trim() || null,
|
|
5
|
+
source_message_id: input.sourceMessageId?.trim() || null,
|
|
6
|
+
};
|
|
7
|
+
}
|
|
8
|
+
export function boardIntentPayloadForTaskMutation(input) {
|
|
9
|
+
return {
|
|
10
|
+
task_id: input.taskId,
|
|
11
|
+
status: input.status ?? null,
|
|
12
|
+
assignee: input.assignee ?? null,
|
|
13
|
+
assignee_agent_key: input.assigneeAgentKey ?? null,
|
|
14
|
+
pr_url: input.prUrl ?? null,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
export function boardIntentPayloadForLeaseAction(input) {
|
|
18
|
+
return {
|
|
19
|
+
task_id: input.taskId,
|
|
20
|
+
action: input.action,
|
|
21
|
+
lease_id: input.leaseId ?? null,
|
|
22
|
+
target_actor_key: input.targetActorKey ?? null,
|
|
23
|
+
target_agent_session_id: input.targetAgentSessionId ?? null,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { buildCompactRoomAgentPrompt, buildRoomAgentPrompt, } from "../shared/room-agent-prompts.js";
|
|
2
|
+
// The MCP server process serves a single agent client, so "delivered once per
|
|
3
|
+
// process" is equivalent to "delivered once per agent session". After the full
|
|
4
|
+
// room-agent instructions have gone out once, every later expansion uses the
|
|
5
|
+
// compact form so long-polling loops stop re-paying the full boilerplate.
|
|
6
|
+
let fullPromptDelivered = false;
|
|
7
|
+
export function dispenseRoomAgentPrompt(kind) {
|
|
8
|
+
if (kind === "join") {
|
|
9
|
+
fullPromptDelivered = true;
|
|
10
|
+
return buildRoomAgentPrompt("join");
|
|
11
|
+
}
|
|
12
|
+
if (fullPromptDelivered) {
|
|
13
|
+
return buildCompactRoomAgentPrompt(kind);
|
|
14
|
+
}
|
|
15
|
+
fullPromptDelivered = true;
|
|
16
|
+
return buildRoomAgentPrompt(kind);
|
|
17
|
+
}
|
|
18
|
+
export function resetRoomAgentPromptDeliveryForTests() {
|
|
19
|
+
fullPromptDelivered = false;
|
|
20
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { spawn } from "child_process";
|
|
2
|
+
import { createServer } from "net";
|
|
3
|
+
const DEFAULT_SERVER_HOST = "127.0.0.1";
|
|
4
|
+
const DEFAULT_TIMEOUT_MS = 15_000;
|
|
5
|
+
function readyUrlFromServerUrl(serverUrl) {
|
|
6
|
+
const url = new URL(serverUrl);
|
|
7
|
+
url.protocol = url.protocol === "wss:" ? "https:" : "http:";
|
|
8
|
+
url.pathname = "/readyz";
|
|
9
|
+
url.search = "";
|
|
10
|
+
url.hash = "";
|
|
11
|
+
return url.toString();
|
|
12
|
+
}
|
|
13
|
+
export async function isServerReady(serverUrl) {
|
|
14
|
+
try {
|
|
15
|
+
const response = await fetch(readyUrlFromServerUrl(serverUrl), {
|
|
16
|
+
signal: AbortSignal.timeout(1_000),
|
|
17
|
+
});
|
|
18
|
+
return response.ok;
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
export async function waitForServer(serverUrl, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
25
|
+
const startedAt = Date.now();
|
|
26
|
+
while (Date.now() - startedAt < timeoutMs) {
|
|
27
|
+
if (await isServerReady(serverUrl)) {
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
31
|
+
}
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
async function allocateLoopbackServerUrl() {
|
|
35
|
+
const server = createServer();
|
|
36
|
+
await new Promise((resolve, reject) => {
|
|
37
|
+
server.once("error", reject);
|
|
38
|
+
server.listen(0, DEFAULT_SERVER_HOST, () => resolve());
|
|
39
|
+
});
|
|
40
|
+
const address = server.address();
|
|
41
|
+
await new Promise((resolve, reject) => {
|
|
42
|
+
server.close((error) => {
|
|
43
|
+
if (error) {
|
|
44
|
+
reject(error);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
resolve();
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
if (!address || typeof address === "string") {
|
|
51
|
+
throw new Error("Unable to allocate a loopback Codex app-server port.");
|
|
52
|
+
}
|
|
53
|
+
return `ws://${DEFAULT_SERVER_HOST}:${address.port}`;
|
|
54
|
+
}
|
|
55
|
+
export async function resolveCodexServerUrl(explicitServerUrl) {
|
|
56
|
+
if (explicitServerUrl) {
|
|
57
|
+
return explicitServerUrl;
|
|
58
|
+
}
|
|
59
|
+
const configuredServerUrl = process.env.LETAGENTS_CODEX_SERVER_URL?.trim();
|
|
60
|
+
if (configuredServerUrl) {
|
|
61
|
+
return configuredServerUrl;
|
|
62
|
+
}
|
|
63
|
+
return allocateLoopbackServerUrl();
|
|
64
|
+
}
|
|
65
|
+
export function launchAppServer(serverUrl, codexBin) {
|
|
66
|
+
const child = spawn(codexBin, ["app-server", "--listen", serverUrl], {
|
|
67
|
+
detached: true,
|
|
68
|
+
stdio: "ignore",
|
|
69
|
+
});
|
|
70
|
+
child.unref();
|
|
71
|
+
return child.pid ?? null;
|
|
72
|
+
}
|
|
73
|
+
export function terminateSpawnedProcess(pid) {
|
|
74
|
+
try {
|
|
75
|
+
if (process.platform !== "win32") {
|
|
76
|
+
process.kill(-pid, "SIGTERM");
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
// Fall back to the direct process below.
|
|
82
|
+
}
|
|
83
|
+
try {
|
|
84
|
+
process.kill(pid, "SIGTERM");
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
// Already gone.
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { configureCodexSessionInspector } from "./session-supervisor.js";
|
|
2
|
+
import { inspectLocalCodexSession } from "./session-inspection.js";
|
|
3
|
+
configureCodexSessionInspector(inspectLocalCodexSession);
|
|
4
|
+
export { deriveCodexLiveSessionStatus } from "./session-status.js";
|
|
5
|
+
export { isCodexAgentSessionMarker, summarizeCodexReasoningNotificationForTest, summarizeCodexRuntimeNotificationForTest, summarizeCodexRuntimeSnapshotForTest, } from "./runtime-summary.js";
|
|
6
|
+
export { bindCodexRuntimeStreamBridgeForAgentSession, scheduleCodexRuntimeStreamBridgeBind, } from "./session-supervisor.js";
|
|
7
|
+
export { inspectLocalCodexSession } from "./session-inspection.js";
|
|
8
|
+
export { toPublicCodexLiveSession } from "./session-mapper.js";
|
|
9
|
+
export { startLocalCodexSession } from "./session-start.js";
|
|
10
|
+
export { stopLocalCodexSession } from "./session-stop.js";
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
function getWebSocketCtor() {
|
|
2
|
+
const ctor = globalThis.WebSocket;
|
|
3
|
+
if (!ctor) {
|
|
4
|
+
throw new Error("Codex live sessions require a Node runtime with global WebSocket support (Node >= 22).");
|
|
5
|
+
}
|
|
6
|
+
return ctor;
|
|
7
|
+
}
|
|
8
|
+
export class RpcClient {
|
|
9
|
+
serverUrl;
|
|
10
|
+
onNotification;
|
|
11
|
+
ws = null;
|
|
12
|
+
nextId = 1;
|
|
13
|
+
pending = new Map();
|
|
14
|
+
constructor(serverUrl, onNotification) {
|
|
15
|
+
this.serverUrl = serverUrl;
|
|
16
|
+
this.onNotification = onNotification;
|
|
17
|
+
}
|
|
18
|
+
async connect() {
|
|
19
|
+
const WS = getWebSocketCtor();
|
|
20
|
+
await new Promise((resolve, reject) => {
|
|
21
|
+
const ws = new WS(this.serverUrl);
|
|
22
|
+
this.ws = ws;
|
|
23
|
+
ws.onopen = () => resolve();
|
|
24
|
+
ws.onerror = () => reject(new Error(`WebSocket error connecting to ${this.serverUrl}`));
|
|
25
|
+
ws.onmessage = (event) => this.handleMessage(String(event.data));
|
|
26
|
+
ws.onclose = () => {
|
|
27
|
+
for (const pending of this.pending.values()) {
|
|
28
|
+
pending.reject(new Error("WebSocket closed"));
|
|
29
|
+
}
|
|
30
|
+
this.pending.clear();
|
|
31
|
+
};
|
|
32
|
+
});
|
|
33
|
+
await this.request("initialize", {
|
|
34
|
+
clientInfo: { name: "letagents-local-codex-session", version: "0.1.0" },
|
|
35
|
+
capabilities: { experimentalApi: true },
|
|
36
|
+
});
|
|
37
|
+
this.ws?.send(JSON.stringify({ method: "initialized" }));
|
|
38
|
+
}
|
|
39
|
+
async request(method, params) {
|
|
40
|
+
const id = this.nextId++;
|
|
41
|
+
const payload = { jsonrpc: "2.0", id, method };
|
|
42
|
+
if (params !== undefined) {
|
|
43
|
+
payload.params = params;
|
|
44
|
+
}
|
|
45
|
+
return new Promise((resolve, reject) => {
|
|
46
|
+
this.pending.set(id, {
|
|
47
|
+
resolve: (value) => resolve(value),
|
|
48
|
+
reject,
|
|
49
|
+
});
|
|
50
|
+
this.ws?.send(JSON.stringify(payload));
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
close() {
|
|
54
|
+
if (this.ws?.readyState === getWebSocketCtor().OPEN) {
|
|
55
|
+
this.ws.close();
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
handleMessage(raw) {
|
|
59
|
+
let message;
|
|
60
|
+
try {
|
|
61
|
+
message = JSON.parse(raw);
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (message.id === undefined) {
|
|
67
|
+
if (typeof message.method === "string") {
|
|
68
|
+
this.onNotification?.({ method: message.method, params: message.params });
|
|
69
|
+
}
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const pending = this.pending.get(message.id);
|
|
73
|
+
if (!pending) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
this.pending.delete(message.id);
|
|
77
|
+
if (message.error) {
|
|
78
|
+
pending.reject(new Error(typeof message.error === "object" && message.error && "message" in message.error
|
|
79
|
+
? String(message.error.message || JSON.stringify(message.error))
|
|
80
|
+
: JSON.stringify(message.error)));
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
pending.resolve(message.result);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import { getCurrentCodexLiveSession, getStoredCodexLiveSession, readLocalState, updateCodexLiveSession, } from "../local-state.js";
|
|
2
|
+
import { encodeRoomIdPath } from "../room-id.js";
|
|
3
|
+
import { apiCall } from "../server/runtime/api.js";
|
|
4
|
+
import { agentSessionCredentials } from "../server/runtime/agent-sessions.js";
|
|
5
|
+
import { RpcClient } from "./rpc-client.js";
|
|
6
|
+
import { isCodexAgentSessionMarker, summarizeCodexRuntimeNotificationForTest, } from "./runtime-summary.js";
|
|
7
|
+
const CODEX_RUNTIME_STREAM_THROTTLE_MS = 750;
|
|
8
|
+
const CODEX_RUNTIME_STREAM_REPEAT_MS = 30_000;
|
|
9
|
+
const CODEX_RUNTIME_STREAM_SNAPSHOT_INTERVAL_MS = 2_000;
|
|
10
|
+
const CODEX_RUNTIME_STREAM_BIND_RETRY_MS = 1_000;
|
|
11
|
+
const CODEX_RUNTIME_STREAM_BIND_RETRY_ATTEMPTS = 30;
|
|
12
|
+
const CODEX_NATIVE_HEARTBEAT_INTERVAL_MS = 15_000;
|
|
13
|
+
export function createCodexRuntimeBridgeController(input) {
|
|
14
|
+
const clients = new Map();
|
|
15
|
+
const snapshotTimers = new Map();
|
|
16
|
+
const bindTimers = new Map();
|
|
17
|
+
const lastPost = new Map();
|
|
18
|
+
const nativeSequence = new Map();
|
|
19
|
+
const nativeLastPostAt = new Map();
|
|
20
|
+
async function codexBridgeApiCall(path, options) {
|
|
21
|
+
return apiCall(path, options);
|
|
22
|
+
}
|
|
23
|
+
function codexWorkerSessionsForRoom(roomId) {
|
|
24
|
+
const state = readLocalState();
|
|
25
|
+
return Object.values(state.agent_sessions ?? {}).filter((session) => session.room_id === roomId &&
|
|
26
|
+
session.session_kind === "worker" &&
|
|
27
|
+
isCodexAgentSessionMarker(session) &&
|
|
28
|
+
Boolean(session.session_id && session.session_token) &&
|
|
29
|
+
!session.ended_at);
|
|
30
|
+
}
|
|
31
|
+
function codexWorkerSessionForLiveSession(session) {
|
|
32
|
+
const state = readLocalState();
|
|
33
|
+
const candidates = codexWorkerSessionsForRoom(session.room_id);
|
|
34
|
+
if (session.agent_session_id) {
|
|
35
|
+
const bound = state.agent_sessions?.[session.agent_session_id] ?? null;
|
|
36
|
+
if (bound &&
|
|
37
|
+
candidates.some((candidate) => candidate.session_id === bound.session_id)) {
|
|
38
|
+
return bound;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
const startedAt = Date.parse(session.started_at);
|
|
42
|
+
const afterLiveSessionStart = candidates
|
|
43
|
+
.filter((candidate) => {
|
|
44
|
+
const createdAt = Date.parse(candidate.created_at);
|
|
45
|
+
return Number.isFinite(startedAt) && Number.isFinite(createdAt) && createdAt >= startedAt - 1000;
|
|
46
|
+
})
|
|
47
|
+
.sort((left, right) => left.created_at.localeCompare(right.created_at));
|
|
48
|
+
if (afterLiveSessionStart.length) {
|
|
49
|
+
return afterLiveSessionStart[0] ?? null;
|
|
50
|
+
}
|
|
51
|
+
const currentSessionId = state.current_agent_session_ids?.[session.room_id];
|
|
52
|
+
const current = currentSessionId ? state.agent_sessions?.[currentSessionId] ?? null : null;
|
|
53
|
+
if (current && candidates.some((candidate) => candidate.session_id === current.session_id)) {
|
|
54
|
+
return current;
|
|
55
|
+
}
|
|
56
|
+
return candidates.sort((left, right) => right.last_seen_at.localeCompare(left.last_seen_at))[0] ?? null;
|
|
57
|
+
}
|
|
58
|
+
async function postReasoningSummary(session, summary) {
|
|
59
|
+
const workerSession = codexWorkerSessionForLiveSession(session);
|
|
60
|
+
if (!workerSession) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
const signature = `${session.session_id}:${summary.summary}:${summary.status}`;
|
|
64
|
+
const previousPost = lastPost.get(session.session_id);
|
|
65
|
+
if (previousPost?.signature === signature &&
|
|
66
|
+
Date.now() - previousPost.postedAt < CODEX_RUNTIME_STREAM_REPEAT_MS) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
if (previousPost && Date.now() - previousPost.postedAt < CODEX_RUNTIME_STREAM_THROTTLE_MS) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
lastPost.set(session.session_id, { signature, postedAt: Date.now() });
|
|
73
|
+
const roomPath = `/rooms/${encodeRoomIdPath(session.room_id)}/reasoning-sessions`;
|
|
74
|
+
const body = {
|
|
75
|
+
actor_label: workerSession.actor_label,
|
|
76
|
+
agent_key: workerSession.agent_key,
|
|
77
|
+
agent_session_id: workerSession.session_id,
|
|
78
|
+
agent_session_token: workerSession.session_token,
|
|
79
|
+
summary: summary.summary,
|
|
80
|
+
goal: "Stream Codex runtime progress for this LetAgents room.",
|
|
81
|
+
checking: summary.checking,
|
|
82
|
+
next_action: summary.next_action,
|
|
83
|
+
status: summary.status,
|
|
84
|
+
};
|
|
85
|
+
if (session.reasoning_session_id) {
|
|
86
|
+
try {
|
|
87
|
+
await codexBridgeApiCall(`${roomPath}/${encodeURIComponent(session.reasoning_session_id)}/updates`, {
|
|
88
|
+
method: "POST",
|
|
89
|
+
body: JSON.stringify(body),
|
|
90
|
+
});
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
updateCodexLiveSession(session.session_id, (current) => ({
|
|
95
|
+
...current,
|
|
96
|
+
reasoning_session_id: null,
|
|
97
|
+
updated_at: new Date().toISOString(),
|
|
98
|
+
}));
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
const created = await codexBridgeApiCall(roomPath, {
|
|
102
|
+
method: "POST",
|
|
103
|
+
body: JSON.stringify(body),
|
|
104
|
+
});
|
|
105
|
+
const reasoningSessionId = created.session?.id;
|
|
106
|
+
if (reasoningSessionId) {
|
|
107
|
+
updateCodexLiveSession(session.session_id, (current) => ({
|
|
108
|
+
...current,
|
|
109
|
+
reasoning_session_id: reasoningSessionId,
|
|
110
|
+
updated_at: new Date().toISOString(),
|
|
111
|
+
}));
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
async function postReasoningUpdate(session, notification) {
|
|
115
|
+
const summary = summarizeCodexRuntimeNotificationForTest(notification);
|
|
116
|
+
await Promise.all([
|
|
117
|
+
postReasoningSummary(session, summary),
|
|
118
|
+
postNativeActivity(session, notification.method, summary.status),
|
|
119
|
+
]);
|
|
120
|
+
}
|
|
121
|
+
async function postNativeActivity(session, method, status, force = false) {
|
|
122
|
+
const workerSession = codexWorkerSessionForLiveSession(session);
|
|
123
|
+
if (!workerSession)
|
|
124
|
+
return;
|
|
125
|
+
const now = Date.now();
|
|
126
|
+
if (!force && now - (nativeLastPostAt.get(session.session_id) ?? 0) < CODEX_RUNTIME_STREAM_THROTTLE_MS)
|
|
127
|
+
return;
|
|
128
|
+
nativeLastPostAt.set(session.session_id, now);
|
|
129
|
+
const sequence = (nativeSequence.get(session.session_id) ?? 0) + 1;
|
|
130
|
+
nativeSequence.set(session.session_id, sequence);
|
|
131
|
+
await codexBridgeApiCall(`/rooms/${encodeRoomIdPath(session.room_id)}/agent-sessions/${encodeURIComponent(workerSession.session_id)}/native-activity`, {
|
|
132
|
+
method: "POST",
|
|
133
|
+
body: JSON.stringify({
|
|
134
|
+
...agentSessionCredentials(workerSession),
|
|
135
|
+
observed_at: new Date(now).toISOString(),
|
|
136
|
+
sequence,
|
|
137
|
+
method,
|
|
138
|
+
status,
|
|
139
|
+
}),
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
function start(session, client) {
|
|
143
|
+
stop(session.session_id);
|
|
144
|
+
clients.set(session.session_id, client);
|
|
145
|
+
void input.inspectSession(session.session_id).catch(() => {
|
|
146
|
+
// The periodic snapshot bridge will retry while the session remains reachable.
|
|
147
|
+
});
|
|
148
|
+
const snapshotTimer = setInterval(() => {
|
|
149
|
+
void input.inspectSession(session.session_id).then((status) => {
|
|
150
|
+
if (!status ||
|
|
151
|
+
!status.server_reachable ||
|
|
152
|
+
input.isTerminalStatus(status.session.status)) {
|
|
153
|
+
stop(session.session_id);
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
const now = Date.now();
|
|
157
|
+
if (now - (nativeLastPostAt.get(session.session_id) ?? 0) >= CODEX_NATIVE_HEARTBEAT_INTERVAL_MS) {
|
|
158
|
+
void postNativeActivity(status.session, "native_harness.heartbeat", status.session.status === "completed" ? "idle" : "working", true).catch(() => undefined);
|
|
159
|
+
}
|
|
160
|
+
}).catch(() => {
|
|
161
|
+
stop(session.session_id);
|
|
162
|
+
});
|
|
163
|
+
}, CODEX_RUNTIME_STREAM_SNAPSHOT_INTERVAL_MS);
|
|
164
|
+
snapshotTimer.unref?.();
|
|
165
|
+
snapshotTimers.set(session.session_id, snapshotTimer);
|
|
166
|
+
}
|
|
167
|
+
async function maybeStart(session) {
|
|
168
|
+
if (clients.has(session.session_id)) {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
if (!codexWorkerSessionForLiveSession(session)) {
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
const client = new RpcClient(session.server_url, (notification) => {
|
|
175
|
+
const latest = getStoredCodexLiveSession(session.session_id) ?? session;
|
|
176
|
+
void postReasoningUpdate(latest, notification).catch(() => {
|
|
177
|
+
// Runtime notifications should never break the worker turn.
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
await client.connect();
|
|
181
|
+
start(session, client);
|
|
182
|
+
}
|
|
183
|
+
async function bindForAgentSession(workerSession) {
|
|
184
|
+
if (!isCodexAgentSessionMarker(workerSession) || workerSession.session_kind !== "worker") {
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
const liveSession = getCurrentCodexLiveSession(workerSession.room_id);
|
|
188
|
+
if (!liveSession || input.isTerminalStatus(liveSession.status)) {
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
191
|
+
const boundSession = updateCodexLiveSession(liveSession.session_id, (current) => ({
|
|
192
|
+
...current,
|
|
193
|
+
agent_session_id: workerSession.session_id,
|
|
194
|
+
updated_at: new Date().toISOString(),
|
|
195
|
+
})) ?? liveSession;
|
|
196
|
+
await maybeStart(boundSession);
|
|
197
|
+
return true;
|
|
198
|
+
}
|
|
199
|
+
function scheduleBind(workerSession, attempts = CODEX_RUNTIME_STREAM_BIND_RETRY_ATTEMPTS) {
|
|
200
|
+
if (!isCodexAgentSessionMarker(workerSession) || workerSession.session_kind !== "worker") {
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
const existing = bindTimers.get(workerSession.session_id);
|
|
204
|
+
if (existing) {
|
|
205
|
+
clearTimeout(existing);
|
|
206
|
+
}
|
|
207
|
+
const attemptBind = (remainingAttempts) => {
|
|
208
|
+
void bindForAgentSession(workerSession)
|
|
209
|
+
.then((bound) => {
|
|
210
|
+
if (bound || remainingAttempts <= 1) {
|
|
211
|
+
bindTimers.delete(workerSession.session_id);
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
const timer = setTimeout(() => attemptBind(remainingAttempts - 1), CODEX_RUNTIME_STREAM_BIND_RETRY_MS);
|
|
215
|
+
timer.unref?.();
|
|
216
|
+
bindTimers.set(workerSession.session_id, timer);
|
|
217
|
+
})
|
|
218
|
+
.catch(() => {
|
|
219
|
+
if (remainingAttempts <= 1) {
|
|
220
|
+
bindTimers.delete(workerSession.session_id);
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
const timer = setTimeout(() => attemptBind(remainingAttempts - 1), CODEX_RUNTIME_STREAM_BIND_RETRY_MS);
|
|
224
|
+
timer.unref?.();
|
|
225
|
+
bindTimers.set(workerSession.session_id, timer);
|
|
226
|
+
});
|
|
227
|
+
};
|
|
228
|
+
attemptBind(attempts);
|
|
229
|
+
}
|
|
230
|
+
function stop(sessionId) {
|
|
231
|
+
const client = clients.get(sessionId);
|
|
232
|
+
if (client) {
|
|
233
|
+
client.close();
|
|
234
|
+
clients.delete(sessionId);
|
|
235
|
+
}
|
|
236
|
+
const snapshotTimer = snapshotTimers.get(sessionId);
|
|
237
|
+
if (snapshotTimer) {
|
|
238
|
+
clearInterval(snapshotTimer);
|
|
239
|
+
snapshotTimers.delete(sessionId);
|
|
240
|
+
}
|
|
241
|
+
lastPost.delete(sessionId);
|
|
242
|
+
nativeSequence.delete(sessionId);
|
|
243
|
+
nativeLastPostAt.delete(sessionId);
|
|
244
|
+
}
|
|
245
|
+
function cleanup() {
|
|
246
|
+
for (const client of clients.values()) {
|
|
247
|
+
client.close();
|
|
248
|
+
}
|
|
249
|
+
clients.clear();
|
|
250
|
+
for (const timer of snapshotTimers.values()) {
|
|
251
|
+
clearInterval(timer);
|
|
252
|
+
}
|
|
253
|
+
snapshotTimers.clear();
|
|
254
|
+
for (const timer of bindTimers.values()) {
|
|
255
|
+
clearTimeout(timer);
|
|
256
|
+
}
|
|
257
|
+
bindTimers.clear();
|
|
258
|
+
lastPost.clear();
|
|
259
|
+
nativeSequence.clear();
|
|
260
|
+
nativeLastPostAt.clear();
|
|
261
|
+
}
|
|
262
|
+
return {
|
|
263
|
+
maybeStart,
|
|
264
|
+
start,
|
|
265
|
+
bindForAgentSession,
|
|
266
|
+
scheduleBind,
|
|
267
|
+
stop,
|
|
268
|
+
cleanup,
|
|
269
|
+
postReasoningSummary,
|
|
270
|
+
};
|
|
271
|
+
}
|