flockbay 0.10.15 → 0.10.17

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 (31) hide show
  1. package/dist/codex/flockbayMcpStdioBridge.cjs +339 -0
  2. package/dist/codex/flockbayMcpStdioBridge.mjs +339 -0
  3. package/dist/{index--o4BPz5o.cjs → index-BxBuBx7C.cjs} +2706 -609
  4. package/dist/{index-CUp3juDS.mjs → index-CHm9r89K.mjs} +2707 -611
  5. package/dist/index.cjs +3 -5
  6. package/dist/index.mjs +3 -5
  7. package/dist/lib.cjs +7 -9
  8. package/dist/lib.d.cts +219 -531
  9. package/dist/lib.d.mts +219 -531
  10. package/dist/lib.mjs +7 -9
  11. package/dist/{runCodex-D3eT-TvB.cjs → runCodex-DuCGwO2K.cjs} +264 -43
  12. package/dist/{runCodex-o6PCbHQ7.mjs → runCodex-DudVDqNh.mjs} +263 -42
  13. package/dist/{runGemini-CBxZp6I7.cjs → runGemini-B25LZ4Cw.cjs} +64 -29
  14. package/dist/{runGemini-Bt0oEj_g.mjs → runGemini-Ddu8UCOS.mjs} +63 -28
  15. package/dist/{types-C-jnUdn_.cjs → types-CGQhv7Z-.cjs} +470 -1146
  16. package/dist/{types-DGd6ea2Z.mjs → types-DuhcLxar.mjs} +469 -1142
  17. package/package.json +1 -1
  18. package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/UnrealMCP/Source/UnrealMCP/Private/Commands/UnrealMCPBlueprintCommands.cpp +195 -6
  19. package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/UnrealMCP/Source/UnrealMCP/Private/Commands/UnrealMCPBlueprintNodeCommands.cpp +376 -5
  20. package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/UnrealMCP/Source/UnrealMCP/Private/Commands/UnrealMCPCommandSchema.cpp +731 -0
  21. package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/UnrealMCP/Source/UnrealMCP/Private/Commands/UnrealMCPCommonUtils.cpp +476 -8
  22. package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/UnrealMCP/Source/UnrealMCP/Private/Commands/UnrealMCPEditorCommands.cpp +1518 -94
  23. package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/UnrealMCP/Source/UnrealMCP/Private/MCPServerRunnable.cpp +7 -4
  24. package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/UnrealMCP/Source/UnrealMCP/Private/UnrealMCPBridge.cpp +150 -112
  25. package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/UnrealMCP/Source/UnrealMCP/Public/Commands/UnrealMCPBlueprintCommands.h +2 -1
  26. package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/UnrealMCP/Source/UnrealMCP/Public/Commands/UnrealMCPBlueprintNodeCommands.h +4 -1
  27. package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/UnrealMCP/Source/UnrealMCP/Public/Commands/UnrealMCPCommandSchema.h +42 -0
  28. package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/UnrealMCP/Source/UnrealMCP/Public/Commands/UnrealMCPEditorCommands.h +21 -0
  29. package/tools/unreal-mcp/upstream/MCPGameProject/Plugins/UnrealMCP/Source/UnrealMCP/UnrealMCP.Build.cs +4 -1
  30. package/dist/flockbayScreenshotGate-DJX3Is5d.mjs +0 -136
  31. package/dist/flockbayScreenshotGate-DkxU24cR.cjs +0 -138
@@ -1,6 +1,6 @@
1
1
  import { useStdout, useInput, Box, Text, render } from 'ink';
2
2
  import React, { useState, useRef, useEffect, useCallback } from 'react';
3
- import { l as logger, A as ApiClient, r as readSettings, p as projectPath, c as configuration, b as packageJson } from './types-DGd6ea2Z.mjs';
3
+ import { l as logger, A as ApiClient, r as readSettings, p as projectPath, c as configuration, b as packageJson } from './types-DuhcLxar.mjs';
4
4
  import { Client } from '@modelcontextprotocol/sdk/client/index.js';
5
5
  import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
6
6
  import { z } from 'zod';
@@ -10,15 +10,13 @@ import fs__default from 'node:fs';
10
10
  import os from 'node:os';
11
11
  import path, { resolve, join } from 'node:path';
12
12
  import { spawnSync } from 'node:child_process';
13
- import { s as shouldCountToolCall, c as consumeToolQuota, f as formatQuotaDeniedReason, h as hashObject, i as initialMachineMetadata, E as ElicitationHub, n as notifyDaemonSessionStarted, M as MessageQueue2, P as PLATFORM_SYSTEM_PROMPT, a as setLatestUserImages, w as withUserImagesMarker, r as registerKillSessionHandler, b as MessageBuffer, d as startFlockbayServer, e as detectUnrealProject, g as buildProjectCapsule, t as trimIdent, j as autoFinalizeCoordinationWorkItem, k as applyCoordinationSideEffectsFromMcpToolResult, l as stopCaffeinate } from './index-CUp3juDS.mjs';
14
- import { d as detectScreenshotsForGate } from './flockbayScreenshotGate-DJX3Is5d.mjs';
13
+ import { s as shouldCountToolCall, c as consumeToolQuota, f as formatQuotaDeniedReason, h as hashObject, i as initialMachineMetadata, E as ElicitationHub, n as notifyDaemonSessionStarted, M as MessageQueue2, P as PLATFORM_SYSTEM_PROMPT, a as setLatestUserImages, w as withUserImagesMarker, r as registerKillSessionHandler, b as MessageBuffer, d as startFlockbayServer, e as detectUnrealProject, g as buildProjectCapsule, t as trimIdent, j as autoFinalizeCoordinationWorkItem, k as detectScreenshotsForGate, l as applyCoordinationSideEffectsFromMcpToolResult, m as stopCaffeinate } from './index-CHm9r89K.mjs';
15
14
  import 'axios';
15
+ import 'node:events';
16
+ import 'socket.io-client';
16
17
  import 'chalk';
17
18
  import 'fs';
18
19
  import 'node:fs/promises';
19
- import 'tweetnacl';
20
- import 'node:events';
21
- import 'socket.io-client';
22
20
  import 'child_process';
23
21
  import 'fs/promises';
24
22
  import 'crypto';
@@ -27,19 +25,18 @@ import 'url';
27
25
  import 'node:process';
28
26
  import 'os';
29
27
  import 'node:net';
30
- import 'expo-server-sdk';
31
28
  import 'node:readline';
32
29
  import 'node:url';
33
30
  import 'ps-list';
34
31
  import 'cross-spawn';
35
32
  import 'tmp';
36
- import 'qrcode-terminal';
37
33
  import 'open';
38
34
  import 'fastify';
39
35
  import 'fastify-type-provider-zod';
40
36
  import '@modelcontextprotocol/sdk/server/mcp.js';
41
37
  import 'node:http';
42
38
  import '@modelcontextprotocol/sdk/server/streamableHttp.js';
39
+ import 'tweetnacl';
43
40
  import 'http';
44
41
  import 'util';
45
42
 
@@ -1286,6 +1283,27 @@ class CodexMcpClient {
1286
1283
  logger.debug("[CodexMCP] Storing conversation for potential resume:", this.conversationId);
1287
1284
  return this.conversationId;
1288
1285
  }
1286
+ /**
1287
+ * Best-effort interrupt for an in-flight Codex request.
1288
+ * Codex MCP does not currently support protocol-level cancellation, so we interrupt the
1289
+ * underlying `codex` process. This should stop streaming and allow the UI abort button
1290
+ * to actually halt the current turn.
1291
+ */
1292
+ interrupt() {
1293
+ const pid = this.transport?.pid ?? null;
1294
+ if (!pid) return false;
1295
+ try {
1296
+ logger.debug("[CodexMCP] Interrupt requested; sending SIGINT to child", { pid });
1297
+ process.kill(pid, "SIGINT");
1298
+ return true;
1299
+ } catch (error) {
1300
+ const e = error;
1301
+ if (e?.code !== "ESRCH") {
1302
+ logger.debug("[CodexMCP] Failed to SIGINT child process", { pid, error });
1303
+ }
1304
+ return false;
1305
+ }
1306
+ }
1289
1307
  async disconnect() {
1290
1308
  if (!this.connected) return;
1291
1309
  const pid = this.transport?.pid ?? null;
@@ -1331,6 +1349,8 @@ class CodexMcpClient {
1331
1349
  this.sessionId = null;
1332
1350
  this.conversationId = null;
1333
1351
  this.authSnapshot = null;
1352
+ this.reconnectPromise = null;
1353
+ this.client = this.createClient();
1334
1354
  logger.debug("[CodexMCP] Disconnected");
1335
1355
  }
1336
1356
  }
@@ -1548,11 +1568,14 @@ class CodexPermissionHandler {
1548
1568
  this.pendingRequests.clear();
1549
1569
  this.aliasToPrimary.clear();
1550
1570
  this.session.updateAgentState((currentState) => {
1551
- const pendingRequests = currentState.requests || {};
1552
- const completedRequests = { ...currentState.completedRequests };
1571
+ const pendingRaw = currentState?.requests;
1572
+ const pendingRequests = pendingRaw && typeof pendingRaw === "object" ? pendingRaw : {};
1573
+ const completedRaw = currentState?.completedRequests;
1574
+ const completedRequests = completedRaw && typeof completedRaw === "object" ? { ...completedRaw } : {};
1553
1575
  for (const [id, request] of Object.entries(pendingRequests)) {
1576
+ const reqObj = request && typeof request === "object" ? request : { value: request };
1554
1577
  completedRequests[id] = {
1555
- ...request,
1578
+ ...reqObj,
1556
1579
  completedAt: Date.now(),
1557
1580
  status: "canceled",
1558
1581
  reason: "Session reset"
@@ -2321,10 +2344,21 @@ async function runCodex(opts) {
2321
2344
  lifecycleStateSince: Date.now(),
2322
2345
  flavor: "codex"
2323
2346
  };
2324
- const response = await api.getOrCreateSession({ tag: sessionTag, metadata, state });
2347
+ const response = opts.sessionId ? await api.getSessionById(opts.sessionId) : await api.getOrCreateSession({ tag: sessionTag, metadata, state });
2325
2348
  const session = api.sessionSyncClient(response);
2349
+ if (opts.sessionId) {
2350
+ session.updateMetadata((currentMetadata) => ({
2351
+ ...currentMetadata,
2352
+ ...metadata,
2353
+ // Preserve user-facing fields set by other clients.
2354
+ name: currentMetadata?.name,
2355
+ summary: currentMetadata?.summary
2356
+ }));
2357
+ }
2326
2358
  const elicitationHub = new ElicitationHub();
2327
2359
  const permissionHandler = new CodexPermissionHandler(session, { elicitationHub });
2360
+ await session.connectAndWait(15e3);
2361
+ session.keepAlive(false, "remote");
2328
2362
  try {
2329
2363
  logger.debug(`[START] Reporting session ${response.id} to daemon`);
2330
2364
  const result = await notifyDaemonSessionStarted(response.id, metadata);
@@ -2339,8 +2373,10 @@ async function runCodex(opts) {
2339
2373
  const messageQueue = new MessageQueue2((mode) => hashCodexSessionModeFromEnhancedMode(mode));
2340
2374
  const autoPrompt = String(process.env.FLOCKBAY_AUTO_PROMPT || "").trim();
2341
2375
  if (autoPrompt) {
2376
+ const autoPromptPermissionModeRaw = String(process.env.FLOCKBAY_AUTO_PROMPT_PERMISSION_MODE || "").trim();
2377
+ const autoPromptPermissionMode = autoPromptPermissionModeRaw === "safe-yolo" || autoPromptPermissionModeRaw === "yolo" || autoPromptPermissionModeRaw === "bypassPermissions" ? autoPromptPermissionModeRaw : "default";
2342
2378
  messageQueue.push(autoPrompt, {
2343
- permissionMode: "default",
2379
+ permissionMode: autoPromptPermissionMode,
2344
2380
  model: void 0,
2345
2381
  appendSystemPrompt: PLATFORM_SYSTEM_PROMPT
2346
2382
  });
@@ -2435,13 +2471,46 @@ async function runCodex(opts) {
2435
2471
  logger.debug("[codex][handles] Failed to serialize active handles:", error);
2436
2472
  }
2437
2473
  }
2474
+ let wasCreated = false;
2475
+ let currentModeHash = null;
2438
2476
  let abortController = new AbortController();
2439
2477
  let shouldExit = false;
2440
2478
  let storedConversationIdForRecovery = null;
2441
- async function handleAbort() {
2479
+ let abortNote = null;
2480
+ let abortNoteSentToSession = false;
2481
+ let abortRequested = false;
2482
+ function normalizeCancelNote(input) {
2483
+ if (typeof input === "string") return input.trim() ? input.trim() : null;
2484
+ if (!input || typeof input !== "object") return null;
2485
+ const note = typeof input.note === "string" ? String(input.note).trim() : "";
2486
+ if (note) return note;
2487
+ const reason = typeof input.reason === "string" ? String(input.reason).trim() : "";
2488
+ if (reason) return reason;
2489
+ return null;
2490
+ }
2491
+ async function handleAbort(note, options) {
2442
2492
  logger.debug("[Codex] Abort requested - stopping current task");
2443
2493
  try {
2494
+ abortRequested = true;
2495
+ const normalizedNote = normalizeCancelNote(note);
2496
+ if (normalizedNote) {
2497
+ abortNote = normalizedNote;
2498
+ abortNoteSentToSession = Boolean(options?.alreadySentToSession);
2499
+ }
2444
2500
  abortController.abort();
2501
+ try {
2502
+ storedConversationIdForRecovery = client.storeConversationForResume() || client.storeSessionForResume();
2503
+ client.interrupt();
2504
+ try {
2505
+ await client.disconnect();
2506
+ } catch (disconnectError) {
2507
+ logger.debug("[Codex] Failed to disconnect Codex MCP after interrupt", disconnectError);
2508
+ }
2509
+ wasCreated = false;
2510
+ currentModeHash = null;
2511
+ } catch (error) {
2512
+ logger.debug("[Codex] Failed to interrupt Codex process on abort", error);
2513
+ }
2445
2514
  messageQueue.reset();
2446
2515
  permissionHandler.reset();
2447
2516
  reasoningProcessor.abort();
@@ -2479,7 +2548,10 @@ async function runCodex(opts) {
2479
2548
  process.exit(1);
2480
2549
  }
2481
2550
  };
2482
- session.rpcHandlerManager.registerHandler("abort", handleAbort);
2551
+ async function handleCancelGeneration(params) {
2552
+ await handleAbort(params);
2553
+ }
2554
+ session.rpcHandlerManager.registerHandler("cancel-generation", handleCancelGeneration);
2483
2555
  registerKillSessionHandler(session.rpcHandlerManager, handleKillSession);
2484
2556
  const messageBuffer = new MessageBuffer();
2485
2557
  const hasTTY = process.stdout.isTTY && process.stdin.isTTY;
@@ -2507,6 +2579,57 @@ async function runCodex(opts) {
2507
2579
  process.stdin.setEncoding("utf8");
2508
2580
  }
2509
2581
  const client = new CodexMcpClient();
2582
+ async function buildReplayPromptFromServerMessages() {
2583
+ try {
2584
+ const rows = await session.listMessages();
2585
+ if (!Array.isArray(rows) || rows.length === 0) return null;
2586
+ const lines = [];
2587
+ const push = (role, text) => {
2588
+ const t = String(text || "").trim();
2589
+ if (!t) return;
2590
+ lines.push(`${role === "user" ? "User" : "Assistant"}: ${t}`);
2591
+ };
2592
+ for (const row of rows.slice(-80)) {
2593
+ const content = row?.content;
2594
+ if (!content || typeof content !== "object") continue;
2595
+ const role = content.role;
2596
+ const payload = content.content;
2597
+ if (role === "user") {
2598
+ if (payload && typeof payload === "object" && payload.type === "text" && typeof payload.text === "string") {
2599
+ push("user", payload.text);
2600
+ }
2601
+ continue;
2602
+ }
2603
+ if (role !== "agent") continue;
2604
+ if (!payload || typeof payload !== "object") continue;
2605
+ if (payload.type === "output") {
2606
+ const data = payload.data;
2607
+ if (data && typeof data === "object" && data.type === "assistant") {
2608
+ const blocks = data.message?.content;
2609
+ if (Array.isArray(blocks)) {
2610
+ const txt = blocks.map((b) => b?.type === "text" && typeof b.text === "string" ? b.text : "").join("").trim();
2611
+ if (txt) push("assistant", txt);
2612
+ }
2613
+ }
2614
+ } else if (payload.type === "codex") {
2615
+ const data = payload.data;
2616
+ if (data && typeof data === "object" && data.type === "message" && typeof data.message === "string") {
2617
+ push("assistant", data.message);
2618
+ }
2619
+ }
2620
+ }
2621
+ const compact = lines.filter((l) => l.length > 0).slice(-24).join("\n");
2622
+ const clipped = compact.length > 1e4 ? compact.slice(compact.length - 1e4) : compact;
2623
+ if (!clipped.trim()) return null;
2624
+ return [
2625
+ "Conversation so far (most recent last). Continue naturally; do not repeat it verbatim unless asked:",
2626
+ clipped
2627
+ ].join("\n");
2628
+ } catch (e) {
2629
+ logger.debug("[Codex] Failed to build replay prompt from server messages", e);
2630
+ return null;
2631
+ }
2632
+ }
2510
2633
  function findCodexResumeFile(conversationId) {
2511
2634
  if (!conversationId) return null;
2512
2635
  try {
@@ -2616,9 +2739,13 @@ async function runCodex(opts) {
2616
2739
  Before doing anything else, call \`mcp__flockbay__read_images\` with:
2617
2740
  ${toolArgs}
2618
2741
 
2619
- Then visually inspect EVERY screenshot and report:
2620
- - One short bullet per image ("Image N: ...") describing what you see
2621
- - Whether the screenshots match the user's request
2742
+ Then visually inspect EVERY screenshot and report (this is for YOUR validation, not ceremony):
2743
+ 1) List 2\u20135 concrete acceptance criteria for the user's request.
2744
+ 2) One short bullet per image ("Image N: ...") describing what you see.
2745
+ 3) For each acceptance criterion: mark it as Verified / Not visible / Failed based ONLY on the screenshots.
2746
+ - If "Not visible", say what evidence is missing.
2747
+ - If "Failed", say what is wrong.
2748
+ 4) If anything is not Verified: state the next tool/action you will take to fix or validate it (do not claim completion yet).
2622
2749
 
2623
2750
  Do not run any other tools in this step.
2624
2751
  `);
@@ -2626,6 +2753,28 @@ async function runCodex(opts) {
2626
2753
  client.setPermissionHandler(permissionHandler);
2627
2754
  const execInflight = /* @__PURE__ */ new Set();
2628
2755
  const execAliasToCallId = /* @__PURE__ */ new Map();
2756
+ const execToolCallEmitted = /* @__PURE__ */ new Set();
2757
+ const forceCloseInflightExecCalls = (opts2) => {
2758
+ if (execInflight.size === 0) return;
2759
+ for (const callId of Array.from(execInflight)) {
2760
+ try {
2761
+ session.sendCodexMessage({
2762
+ type: "tool-call-result",
2763
+ callId,
2764
+ output: { ok: !opts2.isError, reason: opts2.reason, missing_exec_command_end: true },
2765
+ is_error: opts2.isError,
2766
+ id: randomUUID()
2767
+ });
2768
+ } catch (err) {
2769
+ logger.debug("[codex] Failed to force-close exec tool-call", {
2770
+ callId,
2771
+ error: err instanceof Error ? err.message : String(err)
2772
+ });
2773
+ }
2774
+ }
2775
+ execInflight.clear();
2776
+ execAliasToCallId.clear();
2777
+ };
2629
2778
  client.setHandler((msg) => {
2630
2779
  try {
2631
2780
  const preview = typeof msg?.type === "string" ? msg.type : "unknown";
@@ -2735,8 +2884,7 @@ async function runCodex(opts) {
2735
2884
  const out = [];
2736
2885
  const seen = /* @__PURE__ */ new Set();
2737
2886
  for (const v of raw) {
2738
- if (typeof v !== "string") continue;
2739
- const s = v.trim();
2887
+ const s = typeof v === "string" ? v.trim() : typeof v === "number" && Number.isFinite(v) ? String(v) : typeof v === "bigint" ? String(v) : "";
2740
2888
  if (!s || s === "undefined") continue;
2741
2889
  if (seen.has(s)) continue;
2742
2890
  seen.add(s);
@@ -3060,30 +3208,30 @@ async function runCodex(opts) {
3060
3208
  id: randomUUID()
3061
3209
  });
3062
3210
  }
3063
- if (msg.type === "exec_command_begin" || msg.type === "exec_approval_request") {
3211
+ if (msg.type === "exec_command_begin") {
3064
3212
  const callId = resolveExecCallId(msg, true);
3065
3213
  if (!callId) {
3066
- console.error("[codexbash-diag] Missing call id for exec begin/approval event", {
3067
- type: msg.type,
3214
+ console.error("[codexbash-diag] Missing call id for exec begin event", {
3068
3215
  keys: Object.keys(msg || {})
3069
3216
  });
3070
- throw new Error("Missing call id for exec begin/approval event");
3217
+ throw new Error("Missing call id for exec begin event");
3071
3218
  }
3072
- if (msg.type === "exec_command_begin") {
3073
- try {
3074
- console.error("[codexbash-diag] exec begin callId", { callId, aliases: getCallIdAliases(msg) });
3075
- } catch {
3076
- }
3219
+ try {
3220
+ console.error("[codexbash-diag] exec begin callId", { callId, aliases: getCallIdAliases(msg) });
3221
+ } catch {
3077
3222
  }
3078
3223
  markExecStarted(callId, msg);
3079
- let { call_id, callId: _callId, type, ...inputs } = msg;
3080
- session.sendCodexMessage({
3081
- type: "tool-call",
3082
- name: "CodexBash",
3083
- callId,
3084
- input: inputs,
3085
- id: randomUUID()
3086
- });
3224
+ if (!execToolCallEmitted.has(callId)) {
3225
+ execToolCallEmitted.add(callId);
3226
+ let { call_id, callId: _callId, type, ...inputs } = msg;
3227
+ session.sendCodexMessage({
3228
+ type: "tool-call",
3229
+ name: "CodexBash",
3230
+ callId,
3231
+ input: inputs,
3232
+ id: randomUUID()
3233
+ });
3234
+ }
3087
3235
  }
3088
3236
  if (msg.type === "exec_command_end") {
3089
3237
  const callId = resolveExecCallId(msg, false);
@@ -3098,6 +3246,24 @@ async function runCodex(opts) {
3098
3246
  console.error("[codexbash-diag] exec end callId", { callId, aliases: getCallIdAliases(msg) });
3099
3247
  } catch {
3100
3248
  }
3249
+ if (!execToolCallEmitted.has(callId)) {
3250
+ execToolCallEmitted.add(callId);
3251
+ try {
3252
+ let { call_id: call_id2, callId: _callId2, type: type2, ...inputs } = msg;
3253
+ session.sendCodexMessage({
3254
+ type: "tool-call",
3255
+ name: "CodexBash",
3256
+ callId,
3257
+ input: inputs,
3258
+ id: randomUUID()
3259
+ });
3260
+ } catch (err) {
3261
+ logger.debug("[codex] Failed to synthesize exec tool-call begin", {
3262
+ callId,
3263
+ error: err instanceof Error ? err.message : String(err)
3264
+ });
3265
+ }
3266
+ }
3101
3267
  let { call_id, callId: _callId, type, ...output } = msg;
3102
3268
  const sanitized = sanitizeExecCommandEndOutput(output);
3103
3269
  collectScreenshotsForGate({ ...sanitized, cwd: output?.cwd ?? sanitized?.cwd ?? msg?.cwd });
@@ -3172,6 +3338,21 @@ async function runCodex(opts) {
3172
3338
  }
3173
3339
  });
3174
3340
  const flockbayServer = await startFlockbayServer(session, { elicitationHub });
3341
+ const unsubscribeUnrealIssue = (() => {
3342
+ const onIssue = flockbayServer?.unreal?.onIssue;
3343
+ if (typeof onIssue !== "function") return null;
3344
+ return onIssue((event) => {
3345
+ try {
3346
+ const kind = typeof event?.kind === "string" ? event.kind : "unreachable";
3347
+ const base = typeof event?.message === "string" && event.message.trim() ? event.message.trim() : "Unreal Editor issue detected.";
3348
+ const msg = kind === "process_exit" ? `Unreal Editor crashed. ${base}` : `Unreal Editor is not reachable. ${base}`;
3349
+ session.sendSessionEvent({ type: "message", message: msg });
3350
+ void handleAbort(msg, { alreadySentToSession: true });
3351
+ } catch (err) {
3352
+ logger.debug("[Codex] Failed to handle Unreal issue event", err);
3353
+ }
3354
+ });
3355
+ })();
3175
3356
  const bridgeCommand = join(projectPath(), "bin", "flockbay-mcp.mjs");
3176
3357
  const mcpServers = {
3177
3358
  flockbay: {
@@ -3205,8 +3386,8 @@ Error: ${message}`,
3205
3386
  return;
3206
3387
  }
3207
3388
  logger.debug("[codex]: client.connect done");
3208
- let wasCreated = false;
3209
- let currentModeHash = null;
3389
+ wasCreated = false;
3390
+ currentModeHash = null;
3210
3391
  let pending = null;
3211
3392
  let nextExperimentalResume = null;
3212
3393
  while (!shouldExit) {
@@ -3259,6 +3440,8 @@ Error: ${message}`,
3259
3440
  messageBuffer.addMessage(message.message, "user");
3260
3441
  currentModeHash = message.hash;
3261
3442
  let skipAutoFinalize = false;
3443
+ let turnCleanupExecIsError = false;
3444
+ let turnCleanupExecReason = "turn_cleanup";
3262
3445
  try {
3263
3446
  resetScreenshotGateForTurn();
3264
3447
  const overrides = { approvalPolicy: "untrusted", sandbox: "workspace-write" };
@@ -3298,18 +3481,35 @@ Error: ${message}`,
3298
3481
  ].filter((v) => typeof v === "string" && v.trim().length > 0).join("\n\n")
3299
3482
  );
3300
3483
  let resumeFile = null;
3484
+ let resumeSource = null;
3301
3485
  if (nextExperimentalResume) {
3302
3486
  resumeFile = nextExperimentalResume;
3487
+ resumeSource = "mode-change";
3303
3488
  nextExperimentalResume = null;
3304
3489
  logger.debug("[Codex] Using resume file from mode change:", resumeFile);
3305
3490
  } else if (storedConversationIdForRecovery) {
3306
3491
  const recoveryResumeFile = findCodexResumeFile(storedConversationIdForRecovery);
3307
3492
  if (recoveryResumeFile) {
3308
3493
  resumeFile = recoveryResumeFile;
3494
+ resumeSource = "recovery";
3309
3495
  logger.debug("[Codex] Using resume file from previous session (error recovery):", resumeFile);
3310
3496
  }
3311
3497
  storedConversationIdForRecovery = null;
3312
3498
  }
3499
+ const replay = await buildReplayPromptFromServerMessages();
3500
+ if (replay) {
3501
+ const escapedUser = String(message.message || "").trim();
3502
+ const replayTrimmed = replay.trimEnd();
3503
+ const needle = escapedUser ? `User: ${escapedUser}` : "";
3504
+ const replayDeduped = needle && replayTrimmed.endsWith(needle) ? replayTrimmed.slice(0, replayTrimmed.length - needle.length).trimEnd() : replayTrimmed;
3505
+ startConfig.prompt = escapedUser ? `${replayDeduped}
3506
+
3507
+ User: ${escapedUser}` : replayDeduped;
3508
+ if (resumeSource === "recovery") {
3509
+ resumeFile = null;
3510
+ resumeSource = null;
3511
+ }
3512
+ }
3313
3513
  if (resumeFile) {
3314
3514
  startConfig.config.experimental_resume = resumeFile;
3315
3515
  }
@@ -3336,10 +3536,19 @@ Error: ${message}`,
3336
3536
  } catch (error) {
3337
3537
  logger.warn("Error in codex session:", error);
3338
3538
  const isAbortError = error instanceof Error && error.name === "AbortError";
3339
- if (isAbortError) {
3539
+ const treatAsAbort = abortRequested || isAbortError;
3540
+ if (treatAsAbort) {
3340
3541
  skipAutoFinalize = true;
3341
- messageBuffer.addMessage("Aborted by user", "status");
3342
- session.sendSessionEvent({ type: "message", message: "Aborted by user" });
3542
+ turnCleanupExecIsError = true;
3543
+ turnCleanupExecReason = "turn_aborted";
3544
+ const note = abortNote || "Aborted by user";
3545
+ abortNote = null;
3546
+ const alreadySent = abortNoteSentToSession;
3547
+ abortNoteSentToSession = false;
3548
+ abortRequested = false;
3549
+ messageBuffer.addMessage(note, "status");
3550
+ if (!alreadySent) session.sendSessionEvent({ type: "message", message: note });
3551
+ session.sendSessionEvent({ type: "ready" });
3343
3552
  if (!client.hasActiveSession()) {
3344
3553
  wasCreated = false;
3345
3554
  currentModeHash = null;
@@ -3349,6 +3558,8 @@ Error: ${message}`,
3349
3558
  logger.debug("[Codex] Abort completed; keeping active session for next message");
3350
3559
  }
3351
3560
  } else {
3561
+ turnCleanupExecIsError = true;
3562
+ turnCleanupExecReason = "turn_error";
3352
3563
  const errorMessage = error instanceof Error ? error.message : String(error);
3353
3564
  messageBuffer.addMessage(`Error: ${errorMessage}`, "status");
3354
3565
  session.sendSessionEvent({ type: "message", message: `Error: ${errorMessage}` });
@@ -3364,8 +3575,14 @@ Error: ${message}`,
3364
3575
  permissionHandler.reset();
3365
3576
  reasoningProcessor.abort();
3366
3577
  diffProcessor.reset();
3578
+ forceCloseInflightExecCalls({ isError: turnCleanupExecIsError, reason: turnCleanupExecReason });
3579
+ abortRequested = false;
3367
3580
  thinking = false;
3368
3581
  session.keepAlive(thinking, "remote");
3582
+ try {
3583
+ session.flush();
3584
+ } catch {
3585
+ }
3369
3586
  if (!skipAutoFinalize) {
3370
3587
  try {
3371
3588
  const finalizeRes = await autoFinalizeCoordinationWorkItem({
@@ -3396,6 +3613,10 @@ Error: ${message}`,
3396
3613
  } finally {
3397
3614
  logger.debug("[codex]: Final cleanup start");
3398
3615
  logActiveHandles("cleanup-start");
3616
+ try {
3617
+ if (typeof unsubscribeUnrealIssue === "function") unsubscribeUnrealIssue();
3618
+ } catch {
3619
+ }
3399
3620
  try {
3400
3621
  logger.debug("[codex]: sendSessionDeath");
3401
3622
  session.sendSessionDeath();