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.
Files changed (124) hide show
  1. package/README.md +18 -15
  2. package/dist/api/board-intent-payloads.js +25 -0
  3. package/dist/mcp/agent-prompt-delivery.js +20 -0
  4. package/dist/mcp/codex-session/app-server.js +89 -0
  5. package/dist/mcp/codex-session/index.js +10 -0
  6. package/dist/mcp/codex-session/rpc-client.js +85 -0
  7. package/dist/mcp/codex-session/runtime-bridge.js +271 -0
  8. package/dist/mcp/codex-session/runtime-summary.js +220 -0
  9. package/dist/mcp/codex-session/session-inspection.js +100 -0
  10. package/dist/mcp/codex-session/session-mapper.js +51 -0
  11. package/dist/mcp/codex-session/session-start.js +182 -0
  12. package/dist/mcp/codex-session/session-status.js +77 -0
  13. package/dist/mcp/codex-session/session-stop.js +43 -0
  14. package/dist/mcp/codex-session/session-supervisor.js +111 -0
  15. package/dist/mcp/codex-session/start-prompt.js +49 -0
  16. package/dist/mcp/codex-session/types.js +1 -0
  17. package/dist/mcp/codex-session.js +1 -672
  18. package/dist/mcp/config-reader.js +10 -2
  19. package/dist/mcp/git-remote.js +90 -14
  20. package/dist/mcp/local-state/agent-identity.js +26 -0
  21. package/dist/mcp/local-state/agent-sessions.js +66 -0
  22. package/dist/mcp/local-state/auth.js +57 -0
  23. package/dist/mcp/local-state/codex-live-sessions.js +59 -0
  24. package/dist/mcp/local-state/index.js +8 -0
  25. package/dist/mcp/local-state/local-chat.js +808 -0
  26. package/dist/mcp/local-state/room-sessions.js +51 -0
  27. package/dist/mcp/local-state/storage.js +95 -0
  28. package/dist/mcp/local-state/types.js +1 -0
  29. package/dist/mcp/local-state.js +1 -351
  30. package/dist/mcp/managed-agent-providers.js +65 -0
  31. package/dist/mcp/rental-tools/activity.js +37 -0
  32. package/dist/mcp/rental-tools/budget.js +36 -0
  33. package/dist/mcp/rental-tools/context.js +92 -0
  34. package/dist/mcp/rental-tools/lifecycle.js +47 -0
  35. package/dist/mcp/rental-tools/metering.js +73 -0
  36. package/dist/mcp/rental-tools/patches.js +100 -0
  37. package/dist/mcp/rental-tools/provider.js +117 -0
  38. package/dist/mcp/rental-tools/shared.js +19 -0
  39. package/dist/mcp/rental-tools/types.js +1 -0
  40. package/dist/mcp/rental-tools.js +7 -0
  41. package/dist/mcp/repo-visibility.js +8 -6
  42. package/dist/mcp/server/register-tools.js +19 -0
  43. package/dist/mcp/server/repo-context.js +28 -0
  44. package/dist/mcp/server/resources.js +77 -0
  45. package/dist/mcp/server/runtime/agent-sessions.js +173 -0
  46. package/dist/mcp/server/runtime/api.js +104 -0
  47. package/dist/mcp/server/runtime/auth-cache.js +20 -0
  48. package/dist/mcp/server/runtime/device-auth.js +48 -0
  49. package/dist/mcp/server/runtime/identity/config.js +58 -0
  50. package/dist/mcp/server/runtime/identity/directory.js +94 -0
  51. package/dist/mcp/server/runtime/identity/liveness.js +32 -0
  52. package/dist/mcp/server/runtime/identity/names.js +74 -0
  53. package/dist/mcp/server/runtime/identity/public.js +19 -0
  54. package/dist/mcp/server/runtime/identity/state.js +32 -0
  55. package/dist/mcp/server/runtime/identity.js +97 -0
  56. package/dist/mcp/server/runtime/messages.js +160 -0
  57. package/dist/mcp/server/runtime/presence.js +52 -0
  58. package/dist/mcp/server/runtime/room-api.js +42 -0
  59. package/dist/mcp/server/runtime/room-state.js +122 -0
  60. package/dist/mcp/server/runtime/rooms.js +374 -0
  61. package/dist/mcp/server/runtime/supervisor-bridge.js +94 -0
  62. package/dist/mcp/server/runtime/worker-bearer.js +67 -0
  63. package/dist/mcp/server/runtime.js +14 -0
  64. package/dist/mcp/server/tools/agent-sessions.js +415 -0
  65. package/dist/mcp/server/tools/messages/index.js +14 -0
  66. package/dist/mcp/server/tools/messages/message-lookup.js +75 -0
  67. package/dist/mcp/server/tools/messages/read-tool.js +176 -0
  68. package/dist/mcp/server/tools/messages/reasoning-tool.js +179 -0
  69. package/dist/mcp/server/tools/messages/response.js +17 -0
  70. package/dist/mcp/server/tools/messages/send-tool.js +160 -0
  71. package/dist/mcp/server/tools/messages/status-tool.js +87 -0
  72. package/dist/mcp/server/tools/messages/wait-tool.js +403 -0
  73. package/dist/mcp/server/tools/messages.js +1 -0
  74. package/dist/mcp/server/tools/onboarding/device-auth-tools.js +158 -0
  75. package/dist/mcp/server/tools/onboarding/name-tool.js +93 -0
  76. package/dist/mcp/server/tools/onboarding/responses.js +10 -0
  77. package/dist/mcp/server/tools/onboarding/status-tool.js +125 -0
  78. package/dist/mcp/server/tools/onboarding.js +8 -0
  79. package/dist/mcp/server/tools/rental/activity-tools.js +43 -0
  80. package/dist/mcp/server/tools/rental/context-tools.js +43 -0
  81. package/dist/mcp/server/tools/rental/index.js +17 -0
  82. package/dist/mcp/server/tools/rental/metering-tools.js +39 -0
  83. package/dist/mcp/server/tools/rental/patch-tools.js +53 -0
  84. package/dist/mcp/server/tools/rental/provider-tools.js +44 -0
  85. package/dist/mcp/server/tools/rental/response.js +10 -0
  86. package/dist/mcp/server/tools/rental/types.js +1 -0
  87. package/dist/mcp/server/tools/rental.js +1 -0
  88. package/dist/mcp/server/tools/rooms/index.js +5 -0
  89. package/dist/mcp/server/tools/rooms/inspection-tools.js +140 -0
  90. package/dist/mcp/server/tools/rooms/join-tools.js +37 -0
  91. package/dist/mcp/server/tools/rooms/repo-initialization-tool.js +113 -0
  92. package/dist/mcp/server/tools/rooms/repo-visibility-tool.js +20 -0
  93. package/dist/mcp/server/tools/rooms/response.js +10 -0
  94. package/dist/mcp/server/tools/rooms/resume-tool.js +48 -0
  95. package/dist/mcp/server/tools/rooms.js +1 -0
  96. package/dist/mcp/server/tools/tasks/api.js +222 -0
  97. package/dist/mcp/server/tools/tasks/artifact-tools.js +75 -0
  98. package/dist/mcp/server/tools/tasks/board-intent-tools.js +326 -0
  99. package/dist/mcp/server/tools/tasks/board-tools.js +73 -0
  100. package/dist/mcp/server/tools/tasks/context.js +35 -0
  101. package/dist/mcp/server/tools/tasks/event-tools.js +40 -0
  102. package/dist/mcp/server/tools/tasks/index.js +16 -0
  103. package/dist/mcp/server/tools/tasks/lease-tools.js +145 -0
  104. package/dist/mcp/server/tools/tasks/mutation-tools.js +121 -0
  105. package/dist/mcp/server/tools/tasks/response.js +13 -0
  106. package/dist/mcp/server/tools/tasks/schemas.js +68 -0
  107. package/dist/mcp/server/tools/tasks/verdict-tools.js +51 -0
  108. package/dist/mcp/server/tools/tasks.js +1 -0
  109. package/dist/mcp/server.js +12 -3157
  110. package/dist/mcp/sse-client.js +9 -17
  111. package/dist/shared/activation-routing.js +272 -0
  112. package/dist/shared/agent-identity.js +21 -5
  113. package/dist/shared/agent-presence.js +5 -3
  114. package/dist/shared/agent-session-bearer.js +28 -0
  115. package/dist/shared/board-manager-failover.js +16 -0
  116. package/dist/shared/rental/lrt.js +214 -0
  117. package/dist/shared/rental/meter-types.js +14 -0
  118. package/dist/shared/room-agent-prompts.js +14 -1
  119. package/package.json +31 -16
  120. package/dist/mcp/__tests__/config-reader.test.js +0 -93
  121. package/dist/mcp/__tests__/git-remote.test.js +0 -37
  122. package/dist/mcp/__tests__/server-helpers.test.js +0 -121
  123. package/dist/shared/agent-reasoning.js +0 -49
  124. /package/dist/{mcp → shared}/codenames.js +0 -0
@@ -0,0 +1,220 @@
1
+ const CODEX_RUNTIME_STREAM_SOURCE = "codex_app_server";
2
+ const codexReasoningStreams = new Map();
3
+ export function isCodexAgentSessionMarker(session) {
4
+ const runtime = String(session.runtime ?? "").trim().toLowerCase();
5
+ const ideLabel = String(session.ide_label ?? "").trim().toLowerCase();
6
+ const livenessCapability = String(session.liveness_capability ?? "").trim().toLowerCase();
7
+ const toolBridgeId = String(session.tool_bridge_id ?? "").trim().toLowerCase();
8
+ return runtime === "codex" ||
9
+ ideLabel === "codex" ||
10
+ livenessCapability.includes("codex") ||
11
+ /(^|:)codex(:|$)/.test(toolBridgeId);
12
+ }
13
+ function statusForCodexRuntimeMethod(method) {
14
+ if (/blocked|error|failed/i.test(method))
15
+ return "blocked";
16
+ if (/completed|finished|done|stopped|interrupted/i.test(method))
17
+ return "idle";
18
+ return "working";
19
+ }
20
+ function compactRuntimeParam(value) {
21
+ if (!value || typeof value !== "object")
22
+ return null;
23
+ const record = value;
24
+ const item = typeof record.item === "object" && record.item
25
+ ? record.item
26
+ : record;
27
+ const type = typeof item.type === "string" ? item.type : null;
28
+ const name = typeof item.name === "string"
29
+ ? item.name
30
+ : typeof item.command === "string"
31
+ ? item.command
32
+ : null;
33
+ if (type && name)
34
+ return `${type}: ${name}`;
35
+ return type || name;
36
+ }
37
+ function notificationRecord(notification) {
38
+ return notification.params && typeof notification.params === "object"
39
+ ? notification.params
40
+ : {};
41
+ }
42
+ function reasoningStreamKey(record) {
43
+ const threadId = typeof record.threadId === "string" ? record.threadId : "";
44
+ const turnId = typeof record.turnId === "string" ? record.turnId : "";
45
+ const itemId = typeof record.itemId === "string"
46
+ ? record.itemId
47
+ : typeof record.item?.id === "string"
48
+ ? String(record.item.id)
49
+ : "";
50
+ return threadId && turnId && itemId ? `${threadId}:${turnId}:${itemId}` : null;
51
+ }
52
+ function ensureReasoningStream(key) {
53
+ const existing = codexReasoningStreams.get(key);
54
+ if (existing)
55
+ return existing;
56
+ const next = new Map();
57
+ codexReasoningStreams.set(key, next);
58
+ return next;
59
+ }
60
+ function coerceReasoningSummaryParts(value) {
61
+ if (!Array.isArray(value))
62
+ return [];
63
+ return value.map((part) => {
64
+ if (typeof part === "string")
65
+ return part;
66
+ if (part && typeof part === "object" && "text" in part) {
67
+ return String(part.text ?? "");
68
+ }
69
+ return "";
70
+ }).map((part) => part.trim()).filter(Boolean);
71
+ }
72
+ function reasoningSummaryFromStream(stream) {
73
+ return [...stream.entries()]
74
+ .sort(([left], [right]) => left - right)
75
+ .map(([, value]) => value.trim())
76
+ .filter(Boolean)
77
+ .join("\n\n")
78
+ .trim();
79
+ }
80
+ export function summarizeCodexReasoningNotificationForTest(notification) {
81
+ const method = notification.method.trim();
82
+ if (!method.startsWith("item/reasoning/") && method !== "item/started" && method !== "item/completed") {
83
+ return null;
84
+ }
85
+ const record = notificationRecord(notification);
86
+ const item = record.item && typeof record.item === "object"
87
+ ? record.item
88
+ : null;
89
+ const itemType = typeof item?.type === "string" ? item.type : "";
90
+ if ((method === "item/started" || method === "item/completed") && itemType !== "reasoning") {
91
+ return null;
92
+ }
93
+ const key = reasoningStreamKey(record);
94
+ if (!key) {
95
+ return null;
96
+ }
97
+ if (method === "item/reasoning/summaryTextDelta") {
98
+ const summaryIndex = typeof record.summaryIndex === "number" ? record.summaryIndex : 0;
99
+ const delta = typeof record.delta === "string" ? record.delta : "";
100
+ const stream = ensureReasoningStream(key);
101
+ stream.set(summaryIndex, `${stream.get(summaryIndex) ?? ""}${delta}`);
102
+ const summary = reasoningSummaryFromStream(stream);
103
+ return {
104
+ summary: summary || "Codex reasoning summary is streaming.",
105
+ status: "working",
106
+ checking: `${CODEX_RUNTIME_STREAM_SOURCE}: readable reasoning summary`,
107
+ next_action: "Continue streaming the Codex reasoning summary.",
108
+ };
109
+ }
110
+ if (method === "item/reasoning/summaryPartAdded") {
111
+ const summaryIndex = typeof record.summaryIndex === "number" ? record.summaryIndex : 0;
112
+ const stream = ensureReasoningStream(key);
113
+ stream.set(summaryIndex, stream.get(summaryIndex) ?? "");
114
+ return {
115
+ summary: reasoningSummaryFromStream(stream) || "Codex started a new reasoning summary section.",
116
+ status: "working",
117
+ checking: `${CODEX_RUNTIME_STREAM_SOURCE}: reasoning summary section ${summaryIndex + 1}`,
118
+ next_action: "Continue streaming the Codex reasoning summary.",
119
+ };
120
+ }
121
+ if (method === "item/reasoning/textDelta") {
122
+ return {
123
+ summary: "Codex raw reasoning text is streaming.",
124
+ status: "working",
125
+ checking: "Raw reasoning text is available from Codex app-server but hidden by LetAgents by default.",
126
+ next_action: "Wait for readable reasoning summary deltas or other runtime progress.",
127
+ };
128
+ }
129
+ const stream = ensureReasoningStream(key);
130
+ const summaryParts = coerceReasoningSummaryParts(item?.summary);
131
+ if (summaryParts.length) {
132
+ summaryParts.forEach((part, index) => stream.set(index, part));
133
+ }
134
+ const summary = reasoningSummaryFromStream(stream);
135
+ const completed = method === "item/completed";
136
+ if (completed) {
137
+ codexReasoningStreams.delete(key);
138
+ }
139
+ return {
140
+ summary: summary || (completed ? "Codex reasoning summary completed." : "Codex reasoning summary started."),
141
+ status: completed ? "idle" : "working",
142
+ checking: `${CODEX_RUNTIME_STREAM_SOURCE}: readable reasoning item`,
143
+ next_action: completed ? "Waiting for the next Codex runtime event." : "Streaming readable Codex reasoning.",
144
+ };
145
+ }
146
+ export function summarizeCodexRuntimeNotificationForTest(notification) {
147
+ const reasoningSummary = summarizeCodexReasoningNotificationForTest(notification);
148
+ if (reasoningSummary) {
149
+ return reasoningSummary;
150
+ }
151
+ const method = notification.method.trim();
152
+ const detail = compactRuntimeParam(notification.params);
153
+ const readableMethod = method.replaceAll("/", " ");
154
+ const status = statusForCodexRuntimeMethod(method);
155
+ const suffix = detail ? ` (${detail})` : "";
156
+ if (/^thread\//i.test(method)) {
157
+ return {
158
+ summary: `Codex ${readableMethod}${suffix}`,
159
+ status,
160
+ checking: `${CODEX_RUNTIME_STREAM_SOURCE}: ${method}`,
161
+ next_action: status === "idle" ? "Waiting for the next room event." : "Continuing the Codex worker turn.",
162
+ };
163
+ }
164
+ if (/^turn\//i.test(method)) {
165
+ return {
166
+ summary: `Codex ${readableMethod}${suffix}`,
167
+ status,
168
+ checking: `${CODEX_RUNTIME_STREAM_SOURCE}: ${method}`,
169
+ next_action: status === "idle" ? "Turn finished; waiting for room activity." : "Streaming turn progress.",
170
+ };
171
+ }
172
+ if (/^item\//i.test(method)) {
173
+ return {
174
+ summary: `Codex ${readableMethod}${suffix}`,
175
+ status,
176
+ checking: detail ? `Handling ${detail}.` : `${CODEX_RUNTIME_STREAM_SOURCE}: ${method}`,
177
+ next_action: status === "idle" ? "Waiting for the next runtime item." : "Continuing runtime work.",
178
+ };
179
+ }
180
+ return {
181
+ summary: `Codex runtime event: ${readableMethod}${suffix}`,
182
+ status,
183
+ checking: `${CODEX_RUNTIME_STREAM_SOURCE}: ${method}`,
184
+ next_action: status === "idle" ? "Waiting for the next runtime event." : "Continuing runtime work.",
185
+ };
186
+ }
187
+ export function summarizeCodexRuntimeSnapshotForTest(input) {
188
+ const recentItems = input.recentItems ?? [];
189
+ const latestItem = [...recentItems].reverse().find((item) => {
190
+ const type = String(item.type ?? "");
191
+ return type === "agentMessage" || type === "userMessage";
192
+ });
193
+ const latestText = typeof latestItem?.text === "string" ? latestItem.text.trim() : "";
194
+ const latestType = String(latestItem?.type ?? "");
195
+ const turnStatus = typeof input.turnStatus === "string" ? input.turnStatus : "";
196
+ const threadStatus = typeof input.threadStatus === "string" ? input.threadStatus : "";
197
+ const status = statusForCodexRuntimeMethod(`${threadStatus} ${turnStatus} ${latestType}`);
198
+ if (latestText) {
199
+ return {
200
+ summary: latestType === "agentMessage" ? latestText : "Codex worker received room input.",
201
+ status,
202
+ checking: latestType === "agentMessage"
203
+ ? "Latest Codex worker message from app-server snapshot."
204
+ : latestText,
205
+ next_action: status === "idle" ? "Waiting for the next room event." : "Continuing the Codex worker turn.",
206
+ };
207
+ }
208
+ if (turnStatus || threadStatus) {
209
+ const statusText = [threadStatus && `thread ${threadStatus}`, turnStatus && `turn ${turnStatus}`]
210
+ .filter(Boolean)
211
+ .join(", ");
212
+ return {
213
+ summary: `Codex worker ${statusText}.`,
214
+ status,
215
+ checking: `${CODEX_RUNTIME_STREAM_SOURCE}: snapshot`,
216
+ next_action: status === "idle" ? "Waiting for the next room event." : "Monitoring Codex worker progress.",
217
+ };
218
+ }
219
+ return null;
220
+ }
@@ -0,0 +1,100 @@
1
+ import { getCurrentCodexLiveSession, getStoredCodexLiveSession, updateCodexLiveSession, } from "../local-state.js";
2
+ import { isServerReady } from "./app-server.js";
3
+ import { RpcClient } from "./rpc-client.js";
4
+ import { summarizeCodexRuntimeSnapshotForTest } from "./runtime-summary.js";
5
+ import { deriveCodexLiveSessionStatus, extractThreadStatus, extractTurnStatus, isLikelyMaterializingError, isTerminalCodexSessionStatus, summarizeItems, } from "./session-status.js";
6
+ import { clearSessionMonitor, killOwnedAppServer, postCodexRuntimeReasoningSummary, } from "./session-supervisor.js";
7
+ export async function inspectLocalCodexSession(sessionId, roomId) {
8
+ const session = sessionId
9
+ ? getStoredCodexLiveSession(sessionId)
10
+ : getCurrentCodexLiveSession(roomId ?? undefined);
11
+ if (!session) {
12
+ return null;
13
+ }
14
+ const serverReachable = await isServerReady(session.server_url);
15
+ if (!serverReachable) {
16
+ const updated = updateCodexLiveSession(session.session_id, (current) => ({
17
+ ...current,
18
+ status: deriveCodexLiveSessionStatus(current, false, null, null),
19
+ updated_at: new Date().toISOString(),
20
+ })) ?? session;
21
+ if (updated.launched_server) {
22
+ killOwnedAppServer(updated);
23
+ clearSessionMonitor(updated.session_id);
24
+ }
25
+ return {
26
+ session: updated,
27
+ server_reachable: false,
28
+ thread_status: null,
29
+ turn_status: null,
30
+ recent_items: [],
31
+ };
32
+ }
33
+ const client = new RpcClient(session.server_url);
34
+ try {
35
+ await client.connect();
36
+ let read = null;
37
+ try {
38
+ read = await client.request("thread/read", {
39
+ threadId: session.thread_id,
40
+ includeTurns: true,
41
+ });
42
+ }
43
+ catch (error) {
44
+ if (!isLikelyMaterializingError(error)) {
45
+ throw error;
46
+ }
47
+ }
48
+ const turns = read?.thread?.turns ?? [];
49
+ const turn = turns.find((candidate) => candidate.id === session.turn_id) ?? turns[turns.length - 1];
50
+ const threadStatus = extractThreadStatus(read?.thread);
51
+ const turnStatus = extractTurnStatus(turn);
52
+ const recentItems = summarizeItems(turn?.items ?? turn?.output);
53
+ const updated = updateCodexLiveSession(session.session_id, (current) => ({
54
+ ...current,
55
+ status: deriveCodexLiveSessionStatus(current, true, threadStatus, turnStatus),
56
+ last_error: null,
57
+ updated_at: new Date().toISOString(),
58
+ })) ?? session;
59
+ if (isTerminalCodexSessionStatus(updated.status)) {
60
+ killOwnedAppServer(updated);
61
+ clearSessionMonitor(updated.session_id);
62
+ }
63
+ void postCodexRuntimeReasoningSummary(updated, summarizeCodexRuntimeSnapshotForTest({
64
+ threadStatus,
65
+ turnStatus,
66
+ recentItems,
67
+ })).catch(() => {
68
+ // Snapshot-derived reasoning should never break session inspection.
69
+ });
70
+ return {
71
+ session: updated,
72
+ server_reachable: true,
73
+ thread_status: read?.thread?.status ?? null,
74
+ turn_status: turn?.status ?? null,
75
+ recent_items: recentItems,
76
+ };
77
+ }
78
+ catch (error) {
79
+ const updated = updateCodexLiveSession(session.session_id, (current) => ({
80
+ ...current,
81
+ status: "unknown",
82
+ last_error: error instanceof Error ? error.message : String(error),
83
+ updated_at: new Date().toISOString(),
84
+ })) ?? session;
85
+ if (updated.launched_server) {
86
+ killOwnedAppServer(updated);
87
+ clearSessionMonitor(updated.session_id);
88
+ }
89
+ return {
90
+ session: updated,
91
+ server_reachable: true,
92
+ thread_status: null,
93
+ turn_status: null,
94
+ recent_items: [],
95
+ };
96
+ }
97
+ finally {
98
+ client.close();
99
+ }
100
+ }
@@ -0,0 +1,51 @@
1
+ export function toSessionState(input) {
2
+ const now = new Date().toISOString();
3
+ return {
4
+ session_id: input.session_id,
5
+ room_id: input.room_id,
6
+ room_identifier: input.room_identifier,
7
+ room_code: input.room_code ?? null,
8
+ room_display_name: input.room_display_name ?? null,
9
+ joined_via: input.joined_via,
10
+ cwd: input.cwd,
11
+ repo_branch: input.repo_branch ?? null,
12
+ stop_phrase: input.stop_phrase,
13
+ max_minutes: input.max_minutes,
14
+ deadline_utc: input.deadline_utc,
15
+ token: input.token,
16
+ thread_id: input.thread_id,
17
+ turn_id: input.turn_id,
18
+ server_url: input.server_url,
19
+ server_pid: input.server_pid,
20
+ launched_server: input.launched_server,
21
+ codex_bin: input.codex_bin,
22
+ status: "running",
23
+ last_error: null,
24
+ started_at: now,
25
+ updated_at: now,
26
+ };
27
+ }
28
+ export function toPublicCodexLiveSession(session) {
29
+ return {
30
+ session_id: session.session_id,
31
+ room_id: session.room_id,
32
+ room_code: session.room_code ?? null,
33
+ room_display_name: session.room_display_name ?? null,
34
+ joined_via: session.joined_via,
35
+ cwd: session.cwd,
36
+ repo_branch: session.repo_branch ?? null,
37
+ stop_phrase: session.stop_phrase,
38
+ max_minutes: session.max_minutes,
39
+ deadline_utc: session.deadline_utc ?? null,
40
+ thread_id: session.thread_id,
41
+ turn_id: session.turn_id,
42
+ server_url: session.server_url,
43
+ server_pid: session.server_pid ?? null,
44
+ launched_server: session.launched_server,
45
+ agent_session_id: session.agent_session_id ?? null,
46
+ status: session.status,
47
+ last_error: session.last_error ?? null,
48
+ started_at: session.started_at,
49
+ updated_at: session.updated_at,
50
+ };
51
+ }
@@ -0,0 +1,182 @@
1
+ import { randomUUID } from "crypto";
2
+ import { resolve } from "path";
3
+ import { getCurrentCodexLiveSession, getStoredCodexLiveSession, saveCodexLiveSession, updateCodexLiveSession, } from "../local-state.js";
4
+ import { getGitCurrentBranch } from "../git-remote.js";
5
+ import { isServerReady, launchAppServer, resolveCodexServerUrl, terminateSpawnedProcess, waitForServer, } from "./app-server.js";
6
+ import { RpcClient, } from "./rpc-client.js";
7
+ import { summarizeCodexRuntimeNotificationForTest } from "./runtime-summary.js";
8
+ import { isTerminalCodexSessionStatus, parseStartupObservationMs, sleep, STARTUP_POLL_INTERVAL_MS, } from "./session-status.js";
9
+ import { buildStartPrompt, DEFAULT_STOP_PHRASE, formatDeadline, makeToken } from "./start-prompt.js";
10
+ import { inspectLocalCodexSession } from "./session-inspection.js";
11
+ import { toSessionState } from "./session-mapper.js";
12
+ import { forgetLaunchedAppServer, killOwnedAppServer, maybeStartCodexRuntimeBridge, postCodexRuntimeReasoningSummary, registerLaunchedAppServer, scheduleOwnedSessionMonitor, startCodexRuntimeBridge, } from "./session-supervisor.js";
13
+ async function waitForWorkerStartup(session) {
14
+ const observationMs = parseStartupObservationMs();
15
+ const deadline = Date.now() + observationMs;
16
+ let latest = session;
17
+ while (Date.now() < deadline) {
18
+ await sleep(Math.min(STARTUP_POLL_INTERVAL_MS, Math.max(deadline - Date.now(), 0)));
19
+ const inspected = await inspectLocalCodexSession(session.session_id);
20
+ if (!inspected) {
21
+ continue;
22
+ }
23
+ latest = inspected.session;
24
+ if (!inspected.server_reachable || latest.status === "unknown") {
25
+ const reason = !inspected.server_reachable
26
+ ? "app-server became unreachable during startup"
27
+ : "worker status became unknown during startup";
28
+ const failed = updateCodexLiveSession(session.session_id, (current) => ({
29
+ ...current,
30
+ status: "failed",
31
+ last_error: reason,
32
+ updated_at: new Date().toISOString(),
33
+ })) ?? latest;
34
+ throw new Error(`Codex worker exited during startup: ${failed.last_error ?? reason}`);
35
+ }
36
+ if (isTerminalCodexSessionStatus(latest.status)) {
37
+ const reason = latest.status === "completed"
38
+ ? "turn completed before entering the room polling loop"
39
+ : `turn entered ${latest.status}`;
40
+ const failed = updateCodexLiveSession(session.session_id, (current) => ({
41
+ ...current,
42
+ status: "failed",
43
+ last_error: reason,
44
+ updated_at: new Date().toISOString(),
45
+ })) ?? latest;
46
+ throw new Error(`Codex worker exited during startup: ${failed.last_error ?? reason}`);
47
+ }
48
+ }
49
+ return latest;
50
+ }
51
+ export function canReuseCodexLiveSessionForStart(input) {
52
+ const { session } = input;
53
+ if (!session) {
54
+ return false;
55
+ }
56
+ return (session.room_id === input.roomId &&
57
+ resolve(session.cwd) === resolve(input.cwd) &&
58
+ (session.repo_branch ?? null) === input.repoBranch);
59
+ }
60
+ export async function startLocalCodexSession(input) {
61
+ const cwd = resolve(input.cwd || process.cwd());
62
+ const repoBranch = getGitCurrentBranch(cwd);
63
+ const currentSession = getCurrentCodexLiveSession(input.room_id);
64
+ if (currentSession &&
65
+ canReuseCodexLiveSessionForStart({
66
+ session: currentSession,
67
+ roomId: input.room_id,
68
+ cwd,
69
+ repoBranch,
70
+ })) {
71
+ const inspected = await inspectLocalCodexSession(currentSession.session_id);
72
+ if (inspected &&
73
+ (inspected.session.status === "running" || inspected.session.status === "starting")) {
74
+ scheduleOwnedSessionMonitor(inspected.session);
75
+ await maybeStartCodexRuntimeBridge(inspected.session);
76
+ return { session: inspected.session, reused: true };
77
+ }
78
+ }
79
+ const serverUrl = await resolveCodexServerUrl(input.server_url);
80
+ const stopPhrase = input.stop_phrase || DEFAULT_STOP_PHRASE;
81
+ const maxMinutes = Number.isFinite(input.max_minutes) ? Math.max(0, input.max_minutes ?? 0) : 0;
82
+ const codexBin = input.codex_bin || process.env.LETAGENTS_CODEX_BIN || "codex";
83
+ const token = makeToken();
84
+ const deadline = formatDeadline(maxMinutes);
85
+ const launchedServer = !(await isServerReady(serverUrl));
86
+ let serverPid = null;
87
+ let client = null;
88
+ let notificationSession = null;
89
+ let startupSucceeded = false;
90
+ try {
91
+ if (launchedServer) {
92
+ serverPid = launchAppServer(serverUrl, codexBin);
93
+ if (serverPid) {
94
+ registerLaunchedAppServer(serverPid);
95
+ }
96
+ const ready = await waitForServer(serverUrl);
97
+ if (!ready) {
98
+ throw new Error(`Timed out waiting for codex app-server at ${serverUrl}`);
99
+ }
100
+ }
101
+ client = new RpcClient(serverUrl, (notification) => {
102
+ const session = notificationSession
103
+ ? getStoredCodexLiveSession(notificationSession.session_id) ?? notificationSession
104
+ : null;
105
+ if (!session)
106
+ return;
107
+ void postCodexRuntimeReasoningSummary(session, summarizeCodexRuntimeNotificationForTest(notification)).catch(() => {
108
+ // Runtime notifications should never break the worker turn.
109
+ });
110
+ });
111
+ await client.connect();
112
+ const threadStart = await client.request("thread/start", {});
113
+ const threadId = threadStart.thread?.id;
114
+ if (!threadId) {
115
+ throw new Error("Codex app-server did not return a thread id.");
116
+ }
117
+ const prompt = buildStartPrompt({
118
+ room_identifier: input.room_identifier,
119
+ joined_via: input.joined_via,
120
+ cwd,
121
+ repo_branch: repoBranch,
122
+ stop_phrase: stopPhrase,
123
+ token,
124
+ deadline_utc: deadline.utc,
125
+ max_minutes: maxMinutes,
126
+ });
127
+ const turnStart = await client.request("turn/start", {
128
+ threadId,
129
+ cwd,
130
+ approvalPolicy: "never",
131
+ sandboxPolicy: { type: "dangerFullAccess" },
132
+ input: [{ type: "text", text: prompt, text_elements: [] }],
133
+ });
134
+ const turnId = turnStart.turn?.id;
135
+ if (!turnId) {
136
+ throw new Error("Codex app-server did not return a turn id.");
137
+ }
138
+ const session = saveCodexLiveSession(toSessionState({
139
+ session_id: randomUUID(),
140
+ room_id: input.room_id,
141
+ room_identifier: input.room_identifier,
142
+ room_code: input.room_code ?? null,
143
+ room_display_name: input.room_display_name ?? null,
144
+ joined_via: input.joined_via,
145
+ cwd,
146
+ repo_branch: repoBranch,
147
+ stop_phrase: stopPhrase,
148
+ max_minutes: maxMinutes,
149
+ deadline_utc: deadline.utc,
150
+ token,
151
+ thread_id: threadId,
152
+ turn_id: turnId,
153
+ server_url: serverUrl,
154
+ server_pid: serverPid,
155
+ launched_server: launchedServer,
156
+ codex_bin: codexBin,
157
+ }));
158
+ notificationSession = session;
159
+ try {
160
+ const verifiedSession = await waitForWorkerStartup(session);
161
+ scheduleOwnedSessionMonitor(verifiedSession);
162
+ startCodexRuntimeBridge(verifiedSession, client);
163
+ client = null;
164
+ startupSucceeded = true;
165
+ return { session: verifiedSession, reused: false };
166
+ }
167
+ catch (error) {
168
+ killOwnedAppServer(session);
169
+ throw error;
170
+ }
171
+ }
172
+ catch (error) {
173
+ if (!startupSucceeded && launchedServer && serverPid) {
174
+ terminateSpawnedProcess(serverPid);
175
+ forgetLaunchedAppServer(serverPid);
176
+ }
177
+ throw error;
178
+ }
179
+ finally {
180
+ client?.close();
181
+ }
182
+ }
@@ -0,0 +1,77 @@
1
+ const DEFAULT_STARTUP_OBSERVATION_MS = 8_000;
2
+ export const STARTUP_POLL_INTERVAL_MS = 500;
3
+ export function extractTurnStatus(turn) {
4
+ if (!turn) {
5
+ return null;
6
+ }
7
+ if (typeof turn.status === "string") {
8
+ return turn.status;
9
+ }
10
+ if (turn.status && typeof turn.status === "object" && "status" in turn.status) {
11
+ return typeof turn.status.status === "string" ? turn.status.status : null;
12
+ }
13
+ return null;
14
+ }
15
+ export function extractThreadStatus(thread) {
16
+ if (!thread?.status) {
17
+ return null;
18
+ }
19
+ if (typeof thread.status === "string") {
20
+ return thread.status;
21
+ }
22
+ return typeof thread.status.type === "string" ? thread.status.type : null;
23
+ }
24
+ export function summarizeItems(items) {
25
+ return (items ?? []).slice(-6).map((item) => {
26
+ if (item.type === "agentMessage") {
27
+ return { type: item.type, phase: item.phase, text: item.text ?? null };
28
+ }
29
+ if (item.type === "userMessage") {
30
+ return {
31
+ type: item.type,
32
+ text: (item.content ?? []).map((part) => part.text ?? "").join("\n"),
33
+ };
34
+ }
35
+ return { type: item.type ?? "unknown" };
36
+ });
37
+ }
38
+ export function deriveCodexLiveSessionStatus(session, serverReachable, threadStatus, turnStatus) {
39
+ if (threadStatus === "systemError" || threadStatus === "error" || turnStatus === "failed") {
40
+ return "failed";
41
+ }
42
+ if (turnStatus === "completed") {
43
+ return "completed";
44
+ }
45
+ if (turnStatus === "interrupted") {
46
+ return "interrupted";
47
+ }
48
+ if (turnStatus === "inProgress" || threadStatus === "active") {
49
+ return "running";
50
+ }
51
+ if (!serverReachable) {
52
+ return session.status === "completed" || session.status === "interrupted"
53
+ ? session.status
54
+ : "unknown";
55
+ }
56
+ if (session.status === "starting") {
57
+ return "starting";
58
+ }
59
+ return session.status;
60
+ }
61
+ export function isTerminalCodexSessionStatus(status) {
62
+ return status === "completed" || status === "interrupted" || status === "failed";
63
+ }
64
+ export function parseStartupObservationMs() {
65
+ const parsed = Number.parseInt(process.env.LETAGENTS_CODEX_STARTUP_OBSERVATION_MS ?? "", 10);
66
+ if (!Number.isFinite(parsed) || parsed < 0) {
67
+ return DEFAULT_STARTUP_OBSERVATION_MS;
68
+ }
69
+ return parsed;
70
+ }
71
+ export function sleep(ms) {
72
+ return new Promise((resolve) => setTimeout(resolve, ms));
73
+ }
74
+ export function isLikelyMaterializingError(error) {
75
+ const message = error instanceof Error ? error.message : String(error);
76
+ return message.includes("not materialized yet");
77
+ }
@@ -0,0 +1,43 @@
1
+ import { getCurrentCodexLiveSession, getStoredCodexLiveSession, updateCodexLiveSession, } from "../local-state.js";
2
+ import { isServerReady } from "./app-server.js";
3
+ import { RpcClient } from "./rpc-client.js";
4
+ import { clearSessionMonitor, killOwnedAppServer, } from "./session-supervisor.js";
5
+ export async function stopLocalCodexSession(options) {
6
+ const session = options?.session_id
7
+ ? getStoredCodexLiveSession(options.session_id)
8
+ : getCurrentCodexLiveSession(options?.room_id ?? undefined);
9
+ if (!session) {
10
+ return null;
11
+ }
12
+ // Attempt to interrupt the turn via RPC, but gracefully handle a dead server.
13
+ const serverReachable = await isServerReady(session.server_url);
14
+ if (serverReachable) {
15
+ try {
16
+ const client = new RpcClient(session.server_url);
17
+ await client.connect();
18
+ try {
19
+ await client.request("turn/interrupt", {
20
+ threadId: session.thread_id,
21
+ turnId: session.turn_id,
22
+ });
23
+ }
24
+ finally {
25
+ client.close();
26
+ }
27
+ }
28
+ catch {
29
+ // Server may have died between the readiness check and the RPC call.
30
+ }
31
+ }
32
+ const updated = updateCodexLiveSession(session.session_id, (current) => ({
33
+ ...current,
34
+ status: "interrupted",
35
+ last_error: serverReachable ? null : "server unreachable at stop time",
36
+ updated_at: new Date().toISOString(),
37
+ })) ?? session;
38
+ if (options?.shutdown_server || updated.launched_server) {
39
+ killOwnedAppServer(updated);
40
+ }
41
+ clearSessionMonitor(updated.session_id);
42
+ return updated;
43
+ }