webmux 0.35.0 → 0.37.0

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.
@@ -1,13 +1,17 @@
1
1
  // @bun
2
2
  var __defProp = Object.defineProperty;
3
3
  var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
4
+ var __returnValue = (v) => v;
5
+ function __exportSetter(name, newValue) {
6
+ this[name] = __returnValue.bind(null, newValue);
7
+ }
4
8
  var __export = (target, all) => {
5
9
  for (var name in all)
6
10
  __defProp(target, name, {
7
11
  get: all[name],
8
12
  enumerable: true,
9
13
  configurable: true,
10
- set: (newValue) => all[name] = () => newValue
14
+ set: __exportSetter.bind(all, name)
11
15
  });
12
16
  };
13
17
  var __require = import.meta.require;
@@ -6961,7 +6965,7 @@ import { networkInterfaces } from "os";
6961
6965
  // package.json
6962
6966
  var package_default = {
6963
6967
  name: "webmux",
6964
- version: "0.35.0",
6968
+ version: "0.37.0",
6965
6969
  description: "Web dashboard for workmux \u2014 browser UI with embedded terminals, PR monitoring, and CI integration",
6966
6970
  type: "module",
6967
6971
  repository: {
@@ -10990,7 +10994,7 @@ var coerce = {
10990
10994
  date: (arg) => ZodDate.create({ ...arg, coerce: true })
10991
10995
  };
10992
10996
  var NEVER = INVALID;
10993
- // node_modules/.bun/@ts-rest+core@3.52.1+c185e43edea803d3/node_modules/@ts-rest/core/index.esm.mjs
10997
+ // node_modules/.bun/@ts-rest+core@3.52.1+0e383980587f1470/node_modules/@ts-rest/core/index.esm.mjs
10994
10998
  var isZodObjectStrict = (obj) => {
10995
10999
  return typeof (obj === null || obj === undefined ? undefined : obj.passthrough) === "function";
10996
11000
  };
@@ -11388,17 +11392,24 @@ var AgentsUiWorktreeSummarySchema = exports_external.object({
11388
11392
  conversation: WorktreeConversationRefSchema.nullable()
11389
11393
  });
11390
11394
  var AgentsUiConversationMessageRoleSchema = exports_external.enum(["user", "assistant"]);
11391
- var AgentsUiConversationMessageStatusSchema = exports_external.enum(["completed", "inProgress"]);
11392
- var AgentsUiConversationMessageKindSchema = exports_external.enum(["text", "toolUse", "toolResult"]);
11395
+ var AgentsUiConversationMessageStatusSchema = exports_external.enum(["completed", "inProgress", "failed"]);
11396
+ var AgentsUiConversationMessageKindSchema = exports_external.enum(["text", "thinking", "toolUse", "toolResult"]);
11393
11397
  var AgentsUiConversationMessageSchema = exports_external.object({
11394
11398
  id: exports_external.string(),
11395
11399
  turnId: exports_external.string(),
11400
+ order: exports_external.number().int().nonnegative(),
11396
11401
  role: AgentsUiConversationMessageRoleSchema,
11397
11402
  text: exports_external.string(),
11398
11403
  status: AgentsUiConversationMessageStatusSchema,
11399
11404
  createdAt: exports_external.string().nullable(),
11400
- kind: AgentsUiConversationMessageKindSchema.optional(),
11401
- toolName: exports_external.string().optional()
11405
+ kind: AgentsUiConversationMessageKindSchema,
11406
+ phase: exports_external.string().optional(),
11407
+ toolName: exports_external.string().optional(),
11408
+ toolCallId: exports_external.string().optional(),
11409
+ command: exports_external.string().optional(),
11410
+ cwd: exports_external.string().optional(),
11411
+ exitCode: exports_external.number().nullable().optional(),
11412
+ durationMs: exports_external.number().nullable().optional()
11402
11413
  });
11403
11414
  var AgentsUiConversationStateSchema = exports_external.object({
11404
11415
  provider: WorktreeConversationProviderSchema,
@@ -11422,24 +11433,36 @@ var AgentsUiInterruptResponseSchema = exports_external.object({
11422
11433
  turnId: exports_external.string(),
11423
11434
  interrupted: exports_external.literal(true)
11424
11435
  });
11425
- var AgentsUiConversationSnapshotEventSchema = exports_external.object({
11426
- type: exports_external.literal("snapshot"),
11427
- data: AgentsUiWorktreeConversationResponseSchema
11428
- });
11429
11436
  var AgentsUiConversationMessageDeltaEventSchema = exports_external.object({
11430
11437
  type: exports_external.literal("messageDelta"),
11438
+ revision: exports_external.number().int().nonnegative(),
11431
11439
  conversationId: exports_external.string(),
11432
11440
  turnId: exports_external.string(),
11433
11441
  itemId: exports_external.string(),
11442
+ order: exports_external.number().int().nonnegative(),
11434
11443
  delta: exports_external.string()
11435
11444
  });
11445
+ var AgentsUiConversationMessageUpsertEventSchema = exports_external.object({
11446
+ type: exports_external.literal("messageUpsert"),
11447
+ revision: exports_external.number().int().nonnegative(),
11448
+ conversationId: exports_external.string(),
11449
+ message: AgentsUiConversationMessageSchema
11450
+ });
11451
+ var AgentsUiConversationStatusEventSchema = exports_external.object({
11452
+ type: exports_external.literal("conversationStatus"),
11453
+ revision: exports_external.number().int().nonnegative(),
11454
+ conversationId: exports_external.string(),
11455
+ running: exports_external.boolean(),
11456
+ activeTurnId: exports_external.string().nullable()
11457
+ });
11436
11458
  var AgentsUiConversationErrorEventSchema = exports_external.object({
11437
11459
  type: exports_external.literal("error"),
11438
11460
  message: exports_external.string()
11439
11461
  });
11440
11462
  var AgentsUiConversationEventSchema = exports_external.discriminatedUnion("type", [
11441
- AgentsUiConversationSnapshotEventSchema,
11442
11463
  AgentsUiConversationMessageDeltaEventSchema,
11464
+ AgentsUiConversationMessageUpsertEventSchema,
11465
+ AgentsUiConversationStatusEventSchema,
11443
11466
  AgentsUiConversationErrorEventSchema
11444
11467
  ]);
11445
11468
  var WorktreeListResponseSchema = exports_external.object({
@@ -12845,6 +12868,14 @@ function isStringArray(raw) {
12845
12868
  // backend/src/adapters/codex-app-server.ts
12846
12869
  var CodexAppServerApprovalPolicySchema = exports_external.enum(["untrusted", "on-failure", "on-request", "never"]);
12847
12870
  var UnknownValueSchema = exports_external.custom(() => true);
12871
+ var JsonValueSchema = exports_external.union([
12872
+ exports_external.string(),
12873
+ exports_external.number(),
12874
+ exports_external.boolean(),
12875
+ exports_external.null(),
12876
+ exports_external.array(UnknownValueSchema),
12877
+ exports_external.record(UnknownValueSchema)
12878
+ ]);
12848
12879
  var CodexAppServerContentItemSchema = exports_external.object({
12849
12880
  type: exports_external.string(),
12850
12881
  text: exports_external.string().optional()
@@ -12859,9 +12890,103 @@ var CodexAppServerAgentMessageItemSchema = exports_external.object({
12859
12890
  id: exports_external.string(),
12860
12891
  text: exports_external.string().optional(),
12861
12892
  message: exports_external.string().optional(),
12862
- phase: exports_external.string().optional(),
12893
+ phase: exports_external.string().nullable().optional(),
12863
12894
  memoryCitation: UnknownValueSchema.optional()
12864
12895
  });
12896
+ var CodexAppServerCommandActionSchema = exports_external.object({
12897
+ type: exports_external.string(),
12898
+ command: exports_external.string().optional(),
12899
+ path: exports_external.string().nullable().optional()
12900
+ });
12901
+ var CodexAppServerCommandExecutionItemSchema = exports_external.object({
12902
+ type: exports_external.literal("commandExecution"),
12903
+ id: exports_external.string(),
12904
+ command: exports_external.string(),
12905
+ cwd: exports_external.string().nullable(),
12906
+ status: exports_external.enum(["inProgress", "completed", "failed", "declined"]),
12907
+ commandActions: exports_external.array(CodexAppServerCommandActionSchema).default([]),
12908
+ aggregatedOutput: exports_external.string().nullable(),
12909
+ exitCode: exports_external.number().nullable(),
12910
+ durationMs: exports_external.number().nullable()
12911
+ });
12912
+ var CodexAppServerPatchChangeKindSchema = exports_external.union([
12913
+ exports_external.object({ type: exports_external.literal("add") }),
12914
+ exports_external.object({ type: exports_external.literal("delete") }),
12915
+ exports_external.object({ type: exports_external.literal("update"), move_path: exports_external.string().nullable() })
12916
+ ]);
12917
+ var CodexAppServerFileUpdateChangeSchema = exports_external.object({
12918
+ path: exports_external.string(),
12919
+ kind: CodexAppServerPatchChangeKindSchema,
12920
+ diff: exports_external.string()
12921
+ });
12922
+ var CodexAppServerFileChangeItemSchema = exports_external.object({
12923
+ type: exports_external.literal("fileChange"),
12924
+ id: exports_external.string(),
12925
+ changes: exports_external.array(CodexAppServerFileUpdateChangeSchema),
12926
+ status: exports_external.enum(["inProgress", "completed", "failed", "declined"])
12927
+ });
12928
+ var CodexAppServerMcpToolCallResultSchema = exports_external.object({
12929
+ content: exports_external.array(JsonValueSchema),
12930
+ structuredContent: JsonValueSchema,
12931
+ _meta: JsonValueSchema
12932
+ });
12933
+ var CodexAppServerMcpToolCallErrorSchema = exports_external.object({
12934
+ message: exports_external.string()
12935
+ });
12936
+ var CodexAppServerMcpToolCallItemSchema = exports_external.object({
12937
+ type: exports_external.literal("mcpToolCall"),
12938
+ id: exports_external.string(),
12939
+ server: exports_external.string(),
12940
+ tool: exports_external.string(),
12941
+ status: exports_external.enum(["inProgress", "completed", "failed"]),
12942
+ arguments: JsonValueSchema,
12943
+ mcpAppResourceUri: exports_external.string().optional(),
12944
+ pluginId: exports_external.string().nullable(),
12945
+ result: CodexAppServerMcpToolCallResultSchema.nullable(),
12946
+ error: CodexAppServerMcpToolCallErrorSchema.nullable(),
12947
+ durationMs: exports_external.number().nullable()
12948
+ });
12949
+ var CodexAppServerDynamicToolCallContentItemSchema = exports_external.union([
12950
+ exports_external.object({ type: exports_external.literal("inputText"), text: exports_external.string() }),
12951
+ exports_external.object({ type: exports_external.literal("inputImage"), imageUrl: exports_external.string() })
12952
+ ]);
12953
+ var CodexAppServerDynamicToolCallItemSchema = exports_external.object({
12954
+ type: exports_external.literal("dynamicToolCall"),
12955
+ id: exports_external.string(),
12956
+ namespace: exports_external.string().nullable(),
12957
+ tool: exports_external.string(),
12958
+ arguments: JsonValueSchema,
12959
+ status: exports_external.enum(["inProgress", "completed", "failed"]),
12960
+ contentItems: exports_external.array(CodexAppServerDynamicToolCallContentItemSchema).nullable(),
12961
+ success: exports_external.boolean().nullable(),
12962
+ durationMs: exports_external.number().nullable()
12963
+ });
12964
+ var CodexAppServerWebSearchActionSchema = exports_external.union([
12965
+ exports_external.object({ type: exports_external.literal("search"), query: exports_external.string().nullable(), queries: exports_external.array(exports_external.string()).nullable() }),
12966
+ exports_external.object({ type: exports_external.literal("openPage"), url: exports_external.string().nullable() }),
12967
+ exports_external.object({ type: exports_external.literal("findInPage"), url: exports_external.string().nullable(), pattern: exports_external.string().nullable() }),
12968
+ exports_external.object({ type: exports_external.literal("other") })
12969
+ ]);
12970
+ var CodexAppServerWebSearchItemSchema = exports_external.object({
12971
+ type: exports_external.literal("webSearch"),
12972
+ id: exports_external.string(),
12973
+ query: exports_external.string(),
12974
+ action: CodexAppServerWebSearchActionSchema.nullable()
12975
+ });
12976
+ var CodexAppServerIgnoredItemSchema = exports_external.object({
12977
+ type: exports_external.enum([
12978
+ "hookPrompt",
12979
+ "plan",
12980
+ "reasoning",
12981
+ "collabAgentToolCall",
12982
+ "imageView",
12983
+ "imageGeneration",
12984
+ "enteredReviewMode",
12985
+ "exitedReviewMode",
12986
+ "contextCompaction"
12987
+ ]),
12988
+ id: exports_external.string()
12989
+ });
12865
12990
  var CodexAppServerGenericItemSchema = exports_external.object({
12866
12991
  type: exports_external.string(),
12867
12992
  id: exports_external.string()
@@ -12869,6 +12994,12 @@ var CodexAppServerGenericItemSchema = exports_external.object({
12869
12994
  var CodexAppServerThreadItemSchema = exports_external.union([
12870
12995
  CodexAppServerUserMessageItemSchema,
12871
12996
  CodexAppServerAgentMessageItemSchema,
12997
+ CodexAppServerCommandExecutionItemSchema,
12998
+ CodexAppServerFileChangeItemSchema,
12999
+ CodexAppServerMcpToolCallItemSchema,
13000
+ CodexAppServerDynamicToolCallItemSchema,
13001
+ CodexAppServerWebSearchItemSchema,
13002
+ CodexAppServerIgnoredItemSchema,
12872
13003
  CodexAppServerGenericItemSchema
12873
13004
  ]);
12874
13005
  var CodexAppServerTurnSchema = exports_external.object({
@@ -12958,7 +13089,31 @@ var CodexAppServerInitializeResponseSchema = exports_external.object({
12958
13089
  platformFamily: exports_external.string(),
12959
13090
  platformOs: exports_external.string()
12960
13091
  });
12961
-
13092
+ function readCodexAppServerStdoutLines(input) {
13093
+ let buffer = input.buffer + (input.chunk ? input.decoder.decode(input.chunk, { stream: true }) : input.decoder.decode());
13094
+ const lines = [];
13095
+ while (true) {
13096
+ const newlineIndex = buffer.indexOf(`
13097
+ `);
13098
+ if (newlineIndex === -1)
13099
+ break;
13100
+ const line = buffer.slice(0, newlineIndex).trim();
13101
+ buffer = buffer.slice(newlineIndex + 1);
13102
+ if (line.length > 0)
13103
+ lines.push(line);
13104
+ }
13105
+ if (!input.chunk) {
13106
+ const finalLine = buffer.trim();
13107
+ buffer = "";
13108
+ if (finalLine.length > 0)
13109
+ lines.push(finalLine);
13110
+ }
13111
+ return { buffer, lines };
13112
+ }
13113
+ function parseCodexAppServerThreadItem(raw) {
13114
+ const parsed = CodexAppServerThreadItemSchema.safeParse(raw);
13115
+ return parsed.success ? parsed.data : null;
13116
+ }
12962
13117
  class CodexAppServerRequestError extends Error {
12963
13118
  code;
12964
13119
  data;
@@ -12972,7 +13127,6 @@ class CodexAppServerRequestError extends Error {
12972
13127
  class CodexAppServerClient {
12973
13128
  opts;
12974
13129
  encoder = new TextEncoder;
12975
- decoder = new TextDecoder;
12976
13130
  listeners = new Set;
12977
13131
  pending = new Map;
12978
13132
  nextId = 1;
@@ -13059,25 +13213,30 @@ class CodexAppServerClient {
13059
13213
  startStdoutLoop(proc) {
13060
13214
  (async () => {
13061
13215
  const reader = proc.stdout.getReader();
13216
+ const decoder = new TextDecoder;
13062
13217
  let buffer = "";
13063
13218
  try {
13064
13219
  while (true) {
13065
13220
  const { done, value } = await reader.read();
13066
13221
  if (done)
13067
13222
  break;
13068
- buffer += this.decoder.decode(value);
13069
- while (true) {
13070
- const newlineIndex = buffer.indexOf(`
13071
- `);
13072
- if (newlineIndex === -1)
13073
- break;
13074
- const line = buffer.slice(0, newlineIndex).trim();
13075
- buffer = buffer.slice(newlineIndex + 1);
13076
- if (line.length === 0)
13077
- continue;
13223
+ const decoded2 = readCodexAppServerStdoutLines({
13224
+ decoder,
13225
+ buffer,
13226
+ chunk: value
13227
+ });
13228
+ buffer = decoded2.buffer;
13229
+ for (const line of decoded2.lines) {
13078
13230
  this.handleStdoutLine(line);
13079
13231
  }
13080
13232
  }
13233
+ const decoded = readCodexAppServerStdoutLines({
13234
+ decoder,
13235
+ buffer
13236
+ });
13237
+ for (const line of decoded.lines) {
13238
+ this.handleStdoutLine(line);
13239
+ }
13081
13240
  } catch (error) {
13082
13241
  if (this.proc === proc) {
13083
13242
  log.error("[agents] codex app-server stdout reader failed", error);
@@ -13088,12 +13247,13 @@ class CodexAppServerClient {
13088
13247
  startStderrLoop(proc) {
13089
13248
  (async () => {
13090
13249
  const reader = proc.stderr.getReader();
13250
+ const decoder = new TextDecoder;
13091
13251
  try {
13092
13252
  while (true) {
13093
13253
  const { done, value } = await reader.read();
13094
13254
  if (done)
13095
13255
  break;
13096
- const chunk = this.decoder.decode(value).trim();
13256
+ const chunk = decoder.decode(value, { stream: true }).trim();
13097
13257
  if (chunk.length > 0) {
13098
13258
  log.debug(`[agents] codex app-server stderr: ${chunk}`);
13099
13259
  }
@@ -14723,14 +14883,26 @@ async function createLinearIssue(input) {
14723
14883
  }
14724
14884
 
14725
14885
  // backend/src/services/conversation-export-service.ts
14886
+ var WebmuxConversationAttachmentMessageSchema = AgentsUiConversationMessageSchema.extend({
14887
+ order: exports_external.number().int().nonnegative().optional(),
14888
+ kind: AgentsUiConversationMessageKindSchema.optional()
14889
+ });
14726
14890
  var WebmuxConversationAttachmentPayloadSchema = exports_external.object({
14727
14891
  webmux: exports_external.literal(1),
14728
14892
  branch: exports_external.string(),
14729
14893
  baseBranch: exports_external.string().nullable(),
14730
14894
  agent: AgentIdSchema.nullable(),
14731
14895
  createdAt: exports_external.string(),
14732
- conversation: exports_external.array(AgentsUiConversationMessageSchema)
14896
+ conversation: exports_external.array(WebmuxConversationAttachmentMessageSchema).transform((messages) => messages.map((message, order) => ({
14897
+ ...message,
14898
+ order: message.order ?? order,
14899
+ kind: message.kind ?? "text"
14900
+ })))
14733
14901
  });
14902
+ function parseWebmuxConversationAttachmentPayload(raw) {
14903
+ const parsed = WebmuxConversationAttachmentPayloadSchema.safeParse(raw);
14904
+ return parsed.success ? parsed.data : null;
14905
+ }
14734
14906
  var defaultSeedFromLinearDeps = {
14735
14907
  fetchIssueWithAttachments,
14736
14908
  downloadWebmuxAttachment: downloadWebmuxAttachmentDefault
@@ -14907,11 +15079,11 @@ async function downloadWebmuxAttachmentDefault(url) {
14907
15079
  return { ok: false, error: `Asset download failed ${res.status}` };
14908
15080
  }
14909
15081
  const text = await res.text();
14910
- const parsed = WebmuxConversationAttachmentPayloadSchema.safeParse(JSON.parse(text));
14911
- if (!parsed.success) {
15082
+ const parsed = parseWebmuxConversationAttachmentPayload(JSON.parse(text));
15083
+ if (!parsed) {
14912
15084
  return { ok: false, error: "Asset is not a webmux conversation payload" };
14913
15085
  }
14914
- return { ok: true, data: parsed.data };
15086
+ return { ok: true, data: parsed };
14915
15087
  } catch (err) {
14916
15088
  const msg = err instanceof Error ? err.message : String(err);
14917
15089
  return { ok: false, error: msg };
@@ -15500,9 +15672,9 @@ class BunTmuxGateway {
15500
15672
 
15501
15673
  // backend/src/domain/policies.ts
15502
15674
  var INVALID_BRANCH_CHARS_RE = /[~^:?*\[\]\\]+/g;
15503
- var UNSAFE_ENV_KEY_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
15504
- var VALID_WORKTREE_NAME_RE = /^[a-z0-9][a-z0-9\-_./]*$/;
15505
- var VALID_INSTANCE_PREFIX_RE = /^[a-z0-9][a-z0-9\-]*$/;
15675
+ var UNSAFE_ENV_KEY_RE = /^[a-z_][a-z0-9_]*$/i;
15676
+ var VALID_WORKTREE_NAME_RE = /^[a-z0-9][a-z0-9\-_./]*$/i;
15677
+ var VALID_INSTANCE_PREFIX_RE = /^[a-z0-9][a-z0-9\-]*$/i;
15506
15678
  function sanitizeBranchName(raw) {
15507
15679
  return raw.toLowerCase().replace(/\s+/g, "-").replace(INVALID_BRANCH_CHARS_RE, "").replace(/@\{/g, "").replace(/\.{2,}/g, ".").replace(/\/{2,}/g, "/").replace(/-{2,}/g, "-").replace(/^[.\-/]+|[.\-/]+$/g, "").replace(/\.lock$/i, "");
15508
15680
  }
@@ -17769,184 +17941,6 @@ function startAutoPullMonitor(deps, intervalMs) {
17769
17941
  return startSerializedInterval(run, intervalMs);
17770
17942
  }
17771
17943
 
17772
- // backend/src/services/agents-ui-stream-service.ts
17773
- function readNotificationParams(raw) {
17774
- return isRecord3(raw) ? raw : null;
17775
- }
17776
- function readThreadId(raw) {
17777
- return typeof raw === "string" && raw.length > 0 ? raw : null;
17778
- }
17779
- function readNotificationItemType(raw) {
17780
- if (!isRecord3(raw))
17781
- return null;
17782
- return typeof raw.type === "string" ? raw.type : null;
17783
- }
17784
- function readAgentsNotificationThreadId(notification) {
17785
- const params = readNotificationParams(notification.params);
17786
- if (!params)
17787
- return null;
17788
- return readThreadId(params.threadId);
17789
- }
17790
- function buildAgentsUiMessageDeltaEvent(notification) {
17791
- if (notification.method !== "item/agentMessage/delta")
17792
- return null;
17793
- const params = readNotificationParams(notification.params);
17794
- if (!params)
17795
- return null;
17796
- const threadId = readThreadId(params.threadId);
17797
- const turnId = readThreadId(params.turnId);
17798
- const itemId = readThreadId(params.itemId);
17799
- const delta = typeof params.delta === "string" ? params.delta : null;
17800
- if (!threadId || !turnId || !itemId || delta === null)
17801
- return null;
17802
- return {
17803
- type: "messageDelta",
17804
- conversationId: threadId,
17805
- turnId,
17806
- itemId,
17807
- delta
17808
- };
17809
- }
17810
- function shouldRefreshAgentsConversationSnapshot(notification) {
17811
- switch (notification.method) {
17812
- case "turn/started":
17813
- case "turn/completed":
17814
- case "thread/status/changed":
17815
- return readAgentsNotificationThreadId(notification) !== null;
17816
- case "item/completed": {
17817
- const params = readNotificationParams(notification.params);
17818
- if (!params)
17819
- return false;
17820
- const itemType = readNotificationItemType(params.item);
17821
- return itemType === "userMessage" || itemType === "agentMessage";
17822
- }
17823
- default:
17824
- return false;
17825
- }
17826
- }
17827
-
17828
- // backend/src/services/agents-ui-action-service.ts
17829
- function classifyAgentsTerminalWorktreeError(error) {
17830
- const message = error instanceof Error ? error.message : String(error);
17831
- if (message.startsWith("No open tmux window found for worktree: ")) {
17832
- return { status: 409, error: message };
17833
- }
17834
- if (message.startsWith("Worktree not found: ")) {
17835
- return { status: 404, error: message };
17836
- }
17837
- return null;
17838
- }
17839
-
17840
- // backend/src/services/snapshot-service.ts
17841
- function formatElapsedSince(startedAt, now) {
17842
- if (!startedAt)
17843
- return "";
17844
- const startedMs = Date.parse(startedAt);
17845
- if (Number.isNaN(startedMs))
17846
- return "";
17847
- const diffMs = Math.max(0, now().getTime() - startedMs);
17848
- const diffMinutes = Math.floor(diffMs / 60000);
17849
- if (diffMinutes < 1)
17850
- return "0m";
17851
- if (diffMinutes < 60)
17852
- return `${diffMinutes}m`;
17853
- const diffHours = Math.floor(diffMinutes / 60);
17854
- if (diffHours < 24)
17855
- return `${diffHours}h`;
17856
- const diffDays = Math.floor(diffHours / 24);
17857
- return `${diffDays}d`;
17858
- }
17859
- function clonePrEntry(pr) {
17860
- return {
17861
- ...pr,
17862
- ciChecks: pr.ciChecks.map((check) => ({ ...check })),
17863
- comments: pr.comments.map((comment) => ({ ...comment }))
17864
- };
17865
- }
17866
- function mapCreationSnapshot(creating) {
17867
- return creating ? {
17868
- phase: creating.phase
17869
- } : null;
17870
- }
17871
- function mapWorktreeSnapshot(state, now, creating, isArchived, findLinearIssue, findAgentLabel) {
17872
- return {
17873
- branch: state.branch,
17874
- label: state.label,
17875
- ...state.baseBranch ? { baseBranch: state.baseBranch } : {},
17876
- path: state.path,
17877
- dir: state.path,
17878
- archived: isArchived(state.path),
17879
- profile: state.profile,
17880
- agentName: state.agentName,
17881
- agentLabel: findAgentLabel ? findAgentLabel(state.agentName) : state.agentName,
17882
- agentTerminalStale: state.agentTerminalStale,
17883
- mux: state.session.exists,
17884
- dirty: state.git.dirty,
17885
- unpushed: state.git.aheadCount > 0,
17886
- paneCount: state.session.paneCount,
17887
- status: creating ? "creating" : state.agent.lifecycle,
17888
- elapsed: formatElapsedSince(state.agent.lastStartedAt, now),
17889
- services: state.services.map((service) => ({ ...service })),
17890
- prs: state.prs.map((pr) => clonePrEntry(pr)),
17891
- linearIssue: findLinearIssue ? findLinearIssue(state.branch) : null,
17892
- creation: mapCreationSnapshot(creating),
17893
- source: state.source,
17894
- oneshot: state.oneshot
17895
- };
17896
- }
17897
- function mapCreatingWorktreeSnapshot(creating, isArchived, findLinearIssue, findAgentLabel) {
17898
- return {
17899
- branch: creating.branch,
17900
- label: null,
17901
- ...creating.baseBranch ? { baseBranch: creating.baseBranch } : {},
17902
- path: creating.path,
17903
- dir: creating.path,
17904
- archived: isArchived(creating.path),
17905
- profile: creating.profile,
17906
- agentName: creating.agentName,
17907
- agentLabel: findAgentLabel ? findAgentLabel(creating.agentName) : creating.agentName,
17908
- agentTerminalStale: false,
17909
- mux: false,
17910
- dirty: false,
17911
- unpushed: false,
17912
- paneCount: 0,
17913
- status: "creating",
17914
- elapsed: "",
17915
- services: [],
17916
- prs: [],
17917
- linearIssue: findLinearIssue ? findLinearIssue(creating.branch) : null,
17918
- creation: mapCreationSnapshot(creating),
17919
- source: creating.source,
17920
- oneshot: null
17921
- };
17922
- }
17923
- function buildWorktreeSnapshots(input) {
17924
- const now = input.now ?? (() => new Date);
17925
- const isArchived = input.isArchived ?? (() => false);
17926
- const creatingWorktrees = input.creatingWorktrees ?? [];
17927
- const creatingByBranch = new Map(creatingWorktrees.map((worktree) => [worktree.branch, worktree]));
17928
- const runtimeWorktrees = input.runtime.listWorktrees();
17929
- const runtimeBranches = new Set(runtimeWorktrees.map((worktree) => worktree.branch));
17930
- const worktrees = runtimeWorktrees.map((state) => mapWorktreeSnapshot(state, now, creatingByBranch.get(state.branch) ?? null, isArchived, input.findLinearIssue, input.findAgentLabel));
17931
- for (const creating of creatingWorktrees) {
17932
- if (!runtimeBranches.has(creating.branch)) {
17933
- worktrees.push(mapCreatingWorktreeSnapshot(creating, isArchived, input.findLinearIssue, input.findAgentLabel));
17934
- }
17935
- }
17936
- worktrees.sort((left, right) => left.branch.localeCompare(right.branch));
17937
- return worktrees;
17938
- }
17939
- function buildProjectSnapshot(input) {
17940
- return {
17941
- project: {
17942
- name: input.projectName,
17943
- mainBranch: input.mainBranch
17944
- },
17945
- worktrees: buildWorktreeSnapshots(input),
17946
- notifications: input.notifications.map((notification) => ({ ...notification }))
17947
- };
17948
- }
17949
-
17950
17944
  // backend/src/services/agents-ui-service.ts
17951
17945
  function cloneConversationMeta(meta) {
17952
17946
  return meta ? { ...meta } : null;
@@ -17977,43 +17971,1257 @@ function buildAgentsUiWorktreeSummary(worktree, conversation) {
17977
17971
  };
17978
17972
  }
17979
17973
 
17980
- // backend/src/services/worktree-conversation-result.ts
17981
- function ok(data) {
17982
- return { ok: true, data };
17983
- }
17984
- function err(status, error) {
17985
- return { ok: false, status, error };
17986
- }
17987
-
17988
- // backend/src/services/claude-conversation-service.ts
17989
- function isClaudeWorktree(worktree) {
17990
- return worktree.agentName === "claude";
17991
- }
17992
- function isClaudeConversationMeta(meta) {
17993
- return meta?.provider === "claudeCode";
17994
- }
17995
- function buildPendingConversationId(worktree) {
17996
- return `claude-pending:${worktree.path}`;
17974
+ // backend/src/services/codex-session-log-service.ts
17975
+ var TOOL_OUTPUT_TRUNCATE_LIMIT = 12000;
17976
+ function readString2(raw) {
17977
+ return typeof raw === "string" && raw.length > 0 ? raw : null;
17997
17978
  }
17998
- function buildClaudeConversationMeta(sessionId, cwd, now) {
17979
+ function parseLogLine(line) {
17980
+ let parsed;
17981
+ try {
17982
+ parsed = JSON.parse(line);
17983
+ } catch {
17984
+ return null;
17985
+ }
17986
+ if (!isRecord3(parsed))
17987
+ return null;
17999
17988
  return {
18000
- provider: "claudeCode",
18001
- conversationId: sessionId,
18002
- sessionId,
18003
- cwd,
18004
- lastSeenAt: now.toISOString()
17989
+ timestamp: readString2(parsed.timestamp),
17990
+ type: readString2(parsed.type),
17991
+ payload: isRecord3(parsed.payload) ? parsed.payload : null
18005
17992
  };
18006
17993
  }
18007
- function sameConversationMeta(left, right) {
18008
- return left?.provider === right.provider && left.conversationId === right.conversationId && left.cwd === right.cwd;
17994
+ function truncate2(text, limit = TOOL_OUTPUT_TRUNCATE_LIMIT) {
17995
+ if (text.length <= limit)
17996
+ return text;
17997
+ return `${text.slice(0, limit)}... (truncated, ${text.length - limit} more chars)`;
18009
17998
  }
18010
- function normalizeSessionMessages(messages) {
18011
- return messages.map((message) => ({
17999
+ function compactJson2(value) {
18000
+ try {
18001
+ return JSON.stringify(value);
18002
+ } catch {
18003
+ return String(value);
18004
+ }
18005
+ }
18006
+ function readReasoningSummary(raw) {
18007
+ if (!Array.isArray(raw))
18008
+ return "";
18009
+ return raw.map((entry) => {
18010
+ if (typeof entry === "string")
18011
+ return entry;
18012
+ if (!isRecord3(entry))
18013
+ return "";
18014
+ if (typeof entry.text === "string")
18015
+ return entry.text;
18016
+ if (typeof entry.summary === "string")
18017
+ return entry.summary;
18018
+ return "";
18019
+ }).filter((text) => text.trim().length > 0).join(`
18020
+ `).trim();
18021
+ }
18022
+ function parseArgumentsRecord(raw) {
18023
+ if (!raw)
18024
+ return null;
18025
+ let parsed;
18026
+ try {
18027
+ parsed = JSON.parse(raw);
18028
+ } catch {
18029
+ return null;
18030
+ }
18031
+ return isRecord3(parsed) ? parsed : null;
18032
+ }
18033
+ function readToolCommand(toolName, argumentsText) {
18034
+ if (toolName === "apply_patch")
18035
+ return "apply_patch";
18036
+ const args = parseArgumentsRecord(argumentsText);
18037
+ if (!args)
18038
+ return null;
18039
+ if (toolName === "exec_command" && typeof args.cmd === "string")
18040
+ return args.cmd;
18041
+ return null;
18042
+ }
18043
+ function readToolCwd(argumentsText) {
18044
+ const args = parseArgumentsRecord(argumentsText);
18045
+ if (!args)
18046
+ return null;
18047
+ return typeof args.workdir === "string" ? args.workdir : null;
18048
+ }
18049
+ function buildToolUseText(toolName, argumentsText) {
18050
+ const command = readToolCommand(toolName, argumentsText);
18051
+ if (command)
18052
+ return command;
18053
+ return argumentsText?.trim() ?? "";
18054
+ }
18055
+ function readOutputExitCode(output) {
18056
+ const processMatch = output.match(/Process exited with code (-?\d+)/);
18057
+ if (processMatch?.[1])
18058
+ return Number(processMatch[1]);
18059
+ const exitMatch = output.match(/^Exit code: (-?\d+)/m);
18060
+ if (exitMatch?.[1])
18061
+ return Number(exitMatch[1]);
18062
+ return null;
18063
+ }
18064
+ function readOutputStatus(output) {
18065
+ const exitCode = readOutputExitCode(output);
18066
+ if (exitCode !== null)
18067
+ return exitCode === 0 ? "completed" : "failed";
18068
+ return output.startsWith("apply_patch verification failed") ? "failed" : "completed";
18069
+ }
18070
+ function pushMessage(messages, message) {
18071
+ messages.push({
18072
+ ...message,
18073
+ order: messages.length
18074
+ });
18075
+ }
18076
+ function hasDuplicateTextMessage(input) {
18077
+ return input.messages.some((message) => message.turnId === input.turnId && message.role === input.role && message.kind === "text" && message.text === input.text && message.phase === input.phase);
18078
+ }
18079
+ function finalizeToolStatuses(messages) {
18080
+ const resultByCallId = new Map;
18081
+ for (const message of messages) {
18082
+ if (message.kind === "toolResult" && message.toolCallId) {
18083
+ resultByCallId.set(message.toolCallId, message);
18084
+ }
18085
+ }
18086
+ return messages.map((message) => {
18087
+ if (message.kind !== "toolUse" || !message.toolCallId)
18088
+ return message;
18089
+ const result = resultByCallId.get(message.toolCallId);
18090
+ if (!result)
18091
+ return message;
18092
+ return {
18093
+ ...message,
18094
+ status: result.status,
18095
+ ...result.exitCode !== undefined ? { exitCode: result.exitCode } : {},
18096
+ ...result.durationMs !== undefined ? { durationMs: result.durationMs } : {}
18097
+ };
18098
+ });
18099
+ }
18100
+ function parseCodexSessionMessages(text) {
18101
+ const messages = [];
18102
+ const toolCallMetadata = new Map;
18103
+ let currentTurnId = null;
18104
+ let blockIndex = 0;
18105
+ for (const line of text.split(`
18106
+ `)) {
18107
+ const trimmed = line.trim();
18108
+ if (trimmed.length === 0)
18109
+ continue;
18110
+ const record = parseLogLine(trimmed);
18111
+ if (!record?.payload)
18112
+ continue;
18113
+ if (record.type === "event_msg") {
18114
+ const eventType = readString2(record.payload.type);
18115
+ if (eventType === "task_started") {
18116
+ currentTurnId = readString2(record.payload.turn_id);
18117
+ blockIndex = 0;
18118
+ continue;
18119
+ }
18120
+ if (eventType === "task_complete" || eventType === "turn_aborted") {
18121
+ currentTurnId = null;
18122
+ continue;
18123
+ }
18124
+ if (eventType === "user_message" && currentTurnId) {
18125
+ const text2 = readString2(record.payload.message);
18126
+ if (!text2 || hasDuplicateTextMessage({ messages, turnId: currentTurnId, role: "user", text: text2 }))
18127
+ continue;
18128
+ pushMessage(messages, {
18129
+ id: `user:${currentTurnId}:${blockIndex}`,
18130
+ turnId: currentTurnId,
18131
+ role: "user",
18132
+ kind: "text",
18133
+ text: text2,
18134
+ status: "completed",
18135
+ createdAt: record.timestamp
18136
+ });
18137
+ blockIndex += 1;
18138
+ continue;
18139
+ }
18140
+ if (eventType === "agent_message" && currentTurnId) {
18141
+ const text2 = readString2(record.payload.message);
18142
+ if (!text2)
18143
+ continue;
18144
+ const phase = readString2(record.payload.phase) ?? undefined;
18145
+ if (hasDuplicateTextMessage({ messages, turnId: currentTurnId, role: "assistant", text: text2, phase }))
18146
+ continue;
18147
+ pushMessage(messages, {
18148
+ id: `assistant:${currentTurnId}:${blockIndex}`,
18149
+ turnId: currentTurnId,
18150
+ role: "assistant",
18151
+ kind: phase === "analysis" ? "thinking" : "text",
18152
+ ...phase ? { phase } : {},
18153
+ text: text2,
18154
+ status: "completed",
18155
+ createdAt: record.timestamp
18156
+ });
18157
+ blockIndex += 1;
18158
+ }
18159
+ continue;
18160
+ }
18161
+ if (record.type !== "response_item" || !currentTurnId)
18162
+ continue;
18163
+ const payloadType = readString2(record.payload.type);
18164
+ if (payloadType === "reasoning") {
18165
+ const summary = readReasoningSummary(record.payload.summary);
18166
+ if (summary.length === 0)
18167
+ continue;
18168
+ pushMessage(messages, {
18169
+ id: `reasoning:${currentTurnId}:${blockIndex}`,
18170
+ turnId: currentTurnId,
18171
+ role: "assistant",
18172
+ kind: "thinking",
18173
+ phase: "analysis",
18174
+ text: summary,
18175
+ status: "completed",
18176
+ createdAt: record.timestamp
18177
+ });
18178
+ blockIndex += 1;
18179
+ continue;
18180
+ }
18181
+ if (payloadType === "function_call" || payloadType === "custom_tool_call") {
18182
+ const callId = readString2(record.payload.call_id);
18183
+ if (!callId)
18184
+ continue;
18185
+ const toolName = readString2(record.payload.name) ?? "tool";
18186
+ const argumentsText = payloadType === "custom_tool_call" ? typeof record.payload.input === "string" ? record.payload.input : compactJson2(record.payload.input ?? {}) : typeof record.payload.arguments === "string" ? record.payload.arguments : compactJson2(record.payload.arguments ?? {});
18187
+ const command = readToolCommand(toolName, argumentsText);
18188
+ const cwd = payloadType === "custom_tool_call" ? null : readToolCwd(argumentsText);
18189
+ toolCallMetadata.set(callId, {
18190
+ toolName,
18191
+ ...command ? { command } : {},
18192
+ ...cwd ? { cwd } : {}
18193
+ });
18194
+ pushMessage(messages, {
18195
+ id: callId,
18196
+ turnId: currentTurnId,
18197
+ role: "assistant",
18198
+ kind: "toolUse",
18199
+ toolName,
18200
+ toolCallId: callId,
18201
+ text: payloadType === "custom_tool_call" ? toolName : buildToolUseText(toolName, argumentsText),
18202
+ ...command ? { command } : {},
18203
+ ...cwd ? { cwd } : {},
18204
+ status: record.payload.status === "failed" ? "failed" : "completed",
18205
+ createdAt: record.timestamp
18206
+ });
18207
+ blockIndex += 1;
18208
+ continue;
18209
+ }
18210
+ if (payloadType === "function_call_output" || payloadType === "custom_tool_call_output") {
18211
+ const callId = readString2(record.payload.call_id);
18212
+ if (!callId)
18213
+ continue;
18214
+ const metadata = toolCallMetadata.get(callId);
18215
+ const output = typeof record.payload.output === "string" ? record.payload.output.trimEnd() : compactJson2(record.payload.output ?? "");
18216
+ const exitCode = readOutputExitCode(output);
18217
+ pushMessage(messages, {
18218
+ id: `${callId}:result`,
18219
+ turnId: currentTurnId,
18220
+ role: "user",
18221
+ kind: "toolResult",
18222
+ ...metadata?.toolName ? { toolName: metadata.toolName } : {},
18223
+ toolCallId: callId,
18224
+ text: truncate2(output),
18225
+ ...metadata?.command ? { command: metadata.command } : {},
18226
+ ...metadata?.cwd ? { cwd: metadata.cwd } : {},
18227
+ status: readOutputStatus(output),
18228
+ createdAt: record.timestamp,
18229
+ exitCode
18230
+ });
18231
+ blockIndex += 1;
18232
+ }
18233
+ }
18234
+ return finalizeToolStatuses(messages);
18235
+ }
18236
+ async function readCodexSessionMessages(thread) {
18237
+ if (!thread.path)
18238
+ return [];
18239
+ try {
18240
+ const file = Bun.file(thread.path);
18241
+ if (!await file.exists())
18242
+ return [];
18243
+ return parseCodexSessionMessages(await file.text());
18244
+ } catch {
18245
+ return [];
18246
+ }
18247
+ }
18248
+
18249
+ // backend/src/services/worktree-conversation-result.ts
18250
+ function ok(data) {
18251
+ return { ok: true, data };
18252
+ }
18253
+ function err(status, error) {
18254
+ return { ok: false, status, error };
18255
+ }
18256
+
18257
+ // backend/src/services/worktree-conversation-service.ts
18258
+ function resolveCodexAppServerLaunchContext(input) {
18259
+ if (input.worktree.agentName !== "codex" || input.meta.agent !== "codex") {
18260
+ return err(409, "Codex web chat is only available for Codex worktrees");
18261
+ }
18262
+ if (!input.profile) {
18263
+ return err(409, `Profile is missing for Codex web chat: ${input.meta.profile}`);
18264
+ }
18265
+ if (input.meta.runtime !== "host" || input.profile.runtime !== "host") {
18266
+ return err(409, "Codex web chat is only available for host-runtime worktrees. Use the terminal for Docker worktrees.");
18267
+ }
18268
+ if (input.profile.yolo !== true) {
18269
+ return err(409, "Codex web chat requires a yolo profile to preserve the Codex approval policy. Use the terminal for this worktree.");
18270
+ }
18271
+ return ok({
18272
+ approvalPolicy: "never",
18273
+ personality: "pragmatic",
18274
+ sandbox: "danger-full-access"
18275
+ });
18276
+ }
18277
+ function isCodexWorktree(worktree) {
18278
+ return worktree.agentName === "codex";
18279
+ }
18280
+ function isCodexConversationMeta(meta) {
18281
+ return meta?.provider === "codexAppServer";
18282
+ }
18283
+ function toIsoTimestamp(epochSeconds) {
18284
+ if (epochSeconds === null)
18285
+ return null;
18286
+ return new Date(epochSeconds * 1000).toISOString();
18287
+ }
18288
+ function isUserMessageItem(item) {
18289
+ return item.type === "userMessage";
18290
+ }
18291
+ function isAgentMessageItem(item) {
18292
+ return item.type === "agentMessage";
18293
+ }
18294
+ function isCommandExecutionItem(item) {
18295
+ return item.type === "commandExecution";
18296
+ }
18297
+ function isFileChangeItem(item) {
18298
+ return item.type === "fileChange";
18299
+ }
18300
+ function isMcpToolCallItem(item) {
18301
+ return item.type === "mcpToolCall";
18302
+ }
18303
+ function isDynamicToolCallItem(item) {
18304
+ return item.type === "dynamicToolCall";
18305
+ }
18306
+ function isWebSearchItem(item) {
18307
+ return item.type === "webSearch";
18308
+ }
18309
+ function extractUserText(item) {
18310
+ return item.content.map((contentItem) => contentItem.text ?? "").join("").trim();
18311
+ }
18312
+ function extractAgentText(item) {
18313
+ return item.text ?? item.message ?? "";
18314
+ }
18315
+ function commandExecutionStatus(item) {
18316
+ switch (item.status) {
18317
+ case "inProgress":
18318
+ return "inProgress";
18319
+ case "completed":
18320
+ return item.exitCode !== null && item.exitCode !== 0 ? "failed" : "completed";
18321
+ case "failed":
18322
+ case "declined":
18323
+ return "failed";
18324
+ }
18325
+ }
18326
+ function commandExecutionDisplayText(item) {
18327
+ const commands = item.commandActions.map((action) => action.command ?? "").filter((command) => command.length > 0);
18328
+ return commands.length > 0 ? commands.join(" && ") : item.command;
18329
+ }
18330
+ function toolStatus(status) {
18331
+ switch (status) {
18332
+ case "inProgress":
18333
+ return "inProgress";
18334
+ case "completed":
18335
+ return "completed";
18336
+ case "failed":
18337
+ case "declined":
18338
+ return "failed";
18339
+ }
18340
+ }
18341
+ function jsonDisplayText(value) {
18342
+ return typeof value === "string" ? value : JSON.stringify(value, null, 2) ?? "";
18343
+ }
18344
+ function patchChangeLabel(change) {
18345
+ switch (change.kind.type) {
18346
+ case "add":
18347
+ return `add ${change.path}`;
18348
+ case "delete":
18349
+ return `delete ${change.path}`;
18350
+ case "update":
18351
+ return change.kind.move_path ? `move ${change.kind.move_path} -> ${change.path}` : `update ${change.path}`;
18352
+ }
18353
+ }
18354
+ function fileChangeDisplayText(item) {
18355
+ return item.changes.map(patchChangeLabel).join(`
18356
+ `);
18357
+ }
18358
+ function fileChangeResultText(item) {
18359
+ return item.changes.map((change) => change.diff.trimEnd()).filter((diff) => diff.length > 0).join(`
18360
+
18361
+ `);
18362
+ }
18363
+ function mcpContentText(content) {
18364
+ if (isRecord3(content) && typeof content.text === "string")
18365
+ return content.text;
18366
+ return jsonDisplayText(content);
18367
+ }
18368
+ function mcpToolResultText(item) {
18369
+ if (item.error)
18370
+ return item.error.message;
18371
+ if (!item.result)
18372
+ return "";
18373
+ const parts = item.result.content.map(mcpContentText);
18374
+ if (item.result.structuredContent !== null) {
18375
+ parts.push(jsonDisplayText(item.result.structuredContent));
18376
+ }
18377
+ return parts.join(`
18378
+
18379
+ `).trim();
18380
+ }
18381
+ function dynamicToolName(item) {
18382
+ return item.namespace ? `${item.namespace}.${item.tool}` : item.tool;
18383
+ }
18384
+ function dynamicToolContentText(content) {
18385
+ switch (content.type) {
18386
+ case "inputText":
18387
+ return content.text;
18388
+ case "inputImage":
18389
+ return content.imageUrl;
18390
+ }
18391
+ }
18392
+ function dynamicToolResultText(item) {
18393
+ return (item.contentItems ?? []).map(dynamicToolContentText).join(`
18394
+
18395
+ `).trim();
18396
+ }
18397
+ function webSearchDisplayText(item) {
18398
+ const action = item.action;
18399
+ if (!action)
18400
+ return item.query;
18401
+ switch (action.type) {
18402
+ case "search":
18403
+ return action.queries?.join(`
18404
+ `) ?? action.query ?? item.query;
18405
+ case "openPage":
18406
+ return action.url ?? item.query;
18407
+ case "findInPage":
18408
+ return [action.url, action.pattern].filter((part) => part !== null).join(`
18409
+ `);
18410
+ case "other":
18411
+ return item.query;
18412
+ }
18413
+ }
18414
+ function isActiveTurnStatus(status) {
18415
+ return status === "inProgress" || status === "active" || status === "running" || status === "pending" || status === "queued";
18416
+ }
18417
+ function findActiveTurn(thread) {
18418
+ for (let index = thread.turns.length - 1;index >= 0; index -= 1) {
18419
+ const turn = thread.turns[index];
18420
+ if (isActiveTurnStatus(turn.status))
18421
+ return turn;
18422
+ }
18423
+ return null;
18424
+ }
18425
+ function buildCommandExecutionMessages(input) {
18426
+ const { item, turnId, createdAt, order } = input;
18427
+ const status = commandExecutionStatus(item);
18428
+ const toolUse = {
18429
+ id: item.id,
18430
+ turnId,
18431
+ order,
18432
+ role: "assistant",
18433
+ kind: "toolUse",
18434
+ toolName: "shell",
18435
+ toolCallId: item.id,
18436
+ text: commandExecutionDisplayText(item),
18437
+ command: item.command,
18438
+ cwd: item.cwd ?? undefined,
18439
+ status,
18440
+ createdAt,
18441
+ exitCode: item.exitCode,
18442
+ durationMs: item.durationMs
18443
+ };
18444
+ const output = item.aggregatedOutput?.trimEnd() ?? "";
18445
+ if (output.length === 0)
18446
+ return [toolUse];
18447
+ return [
18448
+ toolUse,
18449
+ {
18450
+ id: `${item.id}:result`,
18451
+ turnId,
18452
+ order: order + 1,
18453
+ role: "user",
18454
+ kind: "toolResult",
18455
+ toolName: "shell",
18456
+ toolCallId: item.id,
18457
+ text: output,
18458
+ command: item.command,
18459
+ cwd: item.cwd ?? undefined,
18460
+ status,
18461
+ createdAt,
18462
+ exitCode: item.exitCode,
18463
+ durationMs: item.durationMs
18464
+ }
18465
+ ];
18466
+ }
18467
+ function buildFileChangeMessages(input) {
18468
+ const { item, turnId, createdAt, order } = input;
18469
+ const status = toolStatus(item.status);
18470
+ const toolUse = {
18471
+ id: item.id,
18472
+ turnId,
18473
+ order,
18474
+ role: "assistant",
18475
+ kind: "toolUse",
18476
+ toolName: "file change",
18477
+ toolCallId: item.id,
18478
+ text: fileChangeDisplayText(item),
18479
+ status,
18480
+ createdAt
18481
+ };
18482
+ const resultText = fileChangeResultText(item);
18483
+ if (resultText.length === 0)
18484
+ return [toolUse];
18485
+ return [
18486
+ toolUse,
18487
+ {
18488
+ id: `${item.id}:result`,
18489
+ turnId,
18490
+ order: order + 1,
18491
+ role: "user",
18492
+ kind: "toolResult",
18493
+ toolName: "file change",
18494
+ toolCallId: item.id,
18495
+ text: resultText,
18496
+ status,
18497
+ createdAt
18498
+ }
18499
+ ];
18500
+ }
18501
+ function buildMcpToolCallMessages(input) {
18502
+ const { item, turnId, createdAt, order } = input;
18503
+ const status = item.error ? "failed" : toolStatus(item.status);
18504
+ const toolName = `${item.server}.${item.tool}`;
18505
+ const toolUse = {
18506
+ id: item.id,
18507
+ turnId,
18508
+ order,
18509
+ role: "assistant",
18510
+ kind: "toolUse",
18511
+ toolName,
18512
+ toolCallId: item.id,
18513
+ text: jsonDisplayText(item.arguments),
18514
+ status,
18515
+ createdAt,
18516
+ durationMs: item.durationMs
18517
+ };
18518
+ const resultText = mcpToolResultText(item);
18519
+ if (resultText.length === 0)
18520
+ return [toolUse];
18521
+ return [
18522
+ toolUse,
18523
+ {
18524
+ id: `${item.id}:result`,
18525
+ turnId,
18526
+ order: order + 1,
18527
+ role: "user",
18528
+ kind: "toolResult",
18529
+ toolName,
18530
+ toolCallId: item.id,
18531
+ text: resultText,
18532
+ status,
18533
+ createdAt,
18534
+ durationMs: item.durationMs
18535
+ }
18536
+ ];
18537
+ }
18538
+ function buildDynamicToolCallMessages(input) {
18539
+ const { item, turnId, createdAt, order } = input;
18540
+ const status = item.success === false ? "failed" : toolStatus(item.status);
18541
+ const toolName = dynamicToolName(item);
18542
+ const toolUse = {
18543
+ id: item.id,
18544
+ turnId,
18545
+ order,
18546
+ role: "assistant",
18547
+ kind: "toolUse",
18548
+ toolName,
18549
+ toolCallId: item.id,
18550
+ text: jsonDisplayText(item.arguments),
18551
+ status,
18552
+ createdAt,
18553
+ durationMs: item.durationMs
18554
+ };
18555
+ const resultText = dynamicToolResultText(item);
18556
+ if (resultText.length === 0)
18557
+ return [toolUse];
18558
+ return [
18559
+ toolUse,
18560
+ {
18561
+ id: `${item.id}:result`,
18562
+ turnId,
18563
+ order: order + 1,
18564
+ role: "user",
18565
+ kind: "toolResult",
18566
+ toolName,
18567
+ toolCallId: item.id,
18568
+ text: resultText,
18569
+ status,
18570
+ createdAt,
18571
+ durationMs: item.durationMs
18572
+ }
18573
+ ];
18574
+ }
18575
+ function buildWebSearchMessages(input) {
18576
+ const { item, turnId, createdAt, order } = input;
18577
+ return [{
18578
+ id: item.id,
18579
+ turnId,
18580
+ order,
18581
+ role: "assistant",
18582
+ kind: "toolUse",
18583
+ toolName: "web search",
18584
+ toolCallId: item.id,
18585
+ text: webSearchDisplayText(item),
18586
+ status: "completed",
18587
+ createdAt
18588
+ }];
18589
+ }
18590
+ function buildCodexItemConversationMessages(input) {
18591
+ const { item, turnId, turnStatus, createdAt, order, includeEmptyText = false } = input;
18592
+ if (isUserMessageItem(item)) {
18593
+ const text = extractUserText(item);
18594
+ if (text.length === 0 && !includeEmptyText)
18595
+ return [];
18596
+ return [{
18597
+ id: item.id,
18598
+ turnId,
18599
+ order,
18600
+ role: "user",
18601
+ kind: "text",
18602
+ text,
18603
+ status: "completed",
18604
+ createdAt
18605
+ }];
18606
+ }
18607
+ if (isAgentMessageItem(item)) {
18608
+ const text = extractAgentText(item);
18609
+ if (text.length === 0 && !includeEmptyText)
18610
+ return [];
18611
+ const phase = item.phase ?? undefined;
18612
+ const isThinking = phase === "analysis";
18613
+ return [{
18614
+ id: item.id,
18615
+ turnId,
18616
+ order,
18617
+ role: "assistant",
18618
+ kind: isThinking ? "thinking" : "text",
18619
+ phase,
18620
+ text,
18621
+ status: isActiveTurnStatus(turnStatus) ? "inProgress" : "completed",
18622
+ createdAt
18623
+ }];
18624
+ }
18625
+ if (isCommandExecutionItem(item))
18626
+ return buildCommandExecutionMessages({ item, turnId, createdAt, order });
18627
+ if (isFileChangeItem(item))
18628
+ return buildFileChangeMessages({ item, turnId, createdAt, order });
18629
+ if (isMcpToolCallItem(item))
18630
+ return buildMcpToolCallMessages({ item, turnId, createdAt, order });
18631
+ if (isDynamicToolCallItem(item))
18632
+ return buildDynamicToolCallMessages({ item, turnId, createdAt, order });
18633
+ if (isWebSearchItem(item))
18634
+ return buildWebSearchMessages({ item, turnId, createdAt, order });
18635
+ return [];
18636
+ }
18637
+ function buildConversationMessages(thread) {
18638
+ const messages = [];
18639
+ let order = 0;
18640
+ for (const turn of thread.turns) {
18641
+ for (const item of turn.items) {
18642
+ const itemMessages = buildCodexItemConversationMessages({
18643
+ item,
18644
+ turnId: turn.id,
18645
+ turnStatus: turn.status,
18646
+ createdAt: toIsoTimestamp(isUserMessageItem(item) ? turn.startedAt : turn.completedAt ?? turn.startedAt),
18647
+ order
18648
+ });
18649
+ messages.push(...itemMessages);
18650
+ order += itemMessages.length;
18651
+ }
18652
+ }
18653
+ return messages;
18654
+ }
18655
+ function buildConversationState(thread, sessionMessages = []) {
18656
+ const activeTurn = findActiveTurn(thread);
18657
+ const messages = sessionMessages.length > 0 ? sessionMessages : buildConversationMessages(thread);
18658
+ return {
18659
+ provider: "codexAppServer",
18660
+ conversationId: thread.id,
18661
+ cwd: thread.cwd,
18662
+ running: thread.status.type === "active" || activeTurn !== null,
18663
+ activeTurnId: activeTurn?.id ?? null,
18664
+ messages
18665
+ };
18666
+ }
18667
+ function selectDiscoveredThread(threads) {
18668
+ if (threads.length === 0)
18669
+ return null;
18670
+ return [...threads].sort((left, right) => right.updatedAt - left.updatedAt)[0] ?? null;
18671
+ }
18672
+ function buildConversationMeta(thread, now) {
18673
+ return {
18674
+ provider: "codexAppServer",
18675
+ conversationId: thread.id,
18676
+ threadId: thread.id,
18677
+ cwd: thread.cwd,
18678
+ lastSeenAt: now.toISOString()
18679
+ };
18680
+ }
18681
+ function sameConversationMeta(left, right) {
18682
+ return left?.provider === right.provider && left.conversationId === right.conversationId && left.cwd === right.cwd;
18683
+ }
18684
+ function toWorktreeConversationResponse(worktree, conversationMeta, thread, sessionMessages) {
18685
+ return {
18686
+ worktree: buildAgentsUiWorktreeSummary(worktree, conversationMeta),
18687
+ conversation: buildConversationState(thread, sessionMessages)
18688
+ };
18689
+ }
18690
+
18691
+ class WorktreeConversationService {
18692
+ deps;
18693
+ now;
18694
+ readSessionMessages;
18695
+ readMeta;
18696
+ writeMeta;
18697
+ constructor(deps) {
18698
+ this.deps = deps;
18699
+ this.now = deps.now ?? (() => new Date);
18700
+ this.readSessionMessages = deps.readSessionMessages ?? readCodexSessionMessages;
18701
+ this.readMeta = deps.readMeta ?? readWorktreeMeta;
18702
+ this.writeMeta = deps.writeMeta ?? writeWorktreeMeta;
18703
+ }
18704
+ async attachWorktreeConversation(worktree) {
18705
+ return await this.withResolvedConversation(worktree, true, async ({ conversationMeta, thread }) => {
18706
+ const sessionMessages = await this.readSessionMessages(thread);
18707
+ return ok(toWorktreeConversationResponse(worktree, conversationMeta, thread, sessionMessages));
18708
+ });
18709
+ }
18710
+ async readWorktreeConversation(worktree) {
18711
+ return await this.withResolvedConversation(worktree, false, async ({ conversationMeta, thread }) => {
18712
+ const sessionMessages = await this.readSessionMessages(thread);
18713
+ return ok(toWorktreeConversationResponse(worktree, conversationMeta, thread, sessionMessages));
18714
+ });
18715
+ }
18716
+ async sendWorktreeConversationMessage(worktree, text) {
18717
+ return await this.withResolvedConversation(worktree, true, async ({ thread, launchContext }) => {
18718
+ const started = await this.deps.appServer.turnStart({
18719
+ threadId: thread.id,
18720
+ cwd: worktree.path,
18721
+ approvalPolicy: launchContext.approvalPolicy,
18722
+ input: [{ type: "text", text }]
18723
+ });
18724
+ return ok({
18725
+ conversationId: thread.id,
18726
+ turnId: started.turn.id,
18727
+ running: true
18728
+ });
18729
+ });
18730
+ }
18731
+ async interruptWorktreeConversation(worktree) {
18732
+ return await this.withResolvedConversation(worktree, false, async ({ thread }) => {
18733
+ const conversation = buildConversationState(thread);
18734
+ const turnId = conversation.activeTurnId;
18735
+ if (!turnId) {
18736
+ return err(409, "No active Codex turn to interrupt");
18737
+ }
18738
+ await this.deps.appServer.turnInterrupt({
18739
+ threadId: thread.id,
18740
+ turnId
18741
+ });
18742
+ return ok({
18743
+ conversationId: thread.id,
18744
+ turnId,
18745
+ interrupted: true
18746
+ });
18747
+ });
18748
+ }
18749
+ async withResolvedConversation(worktree, allowCreate, fn) {
18750
+ if (!isCodexWorktree(worktree)) {
18751
+ return err(409, "Worktree chat is only available for Codex worktrees");
18752
+ }
18753
+ try {
18754
+ const resolved = await this.resolveConversation(worktree, allowCreate);
18755
+ if (!resolved.ok)
18756
+ return resolved;
18757
+ return await fn(resolved.data);
18758
+ } catch (error) {
18759
+ const message = error instanceof Error ? error.message : String(error);
18760
+ return err(502, message);
18761
+ }
18762
+ }
18763
+ async resolveConversation(worktree, allowCreate) {
18764
+ const gitDir = this.deps.git.resolveWorktreeGitDir(worktree.path);
18765
+ const meta = await this.readMeta(gitDir);
18766
+ if (!meta) {
18767
+ return err(409, "Worktree metadata is missing");
18768
+ }
18769
+ const launchContextResult = await this.deps.resolveLaunchContext({ worktree, meta });
18770
+ if (!launchContextResult.ok)
18771
+ return launchContextResult;
18772
+ const launchContext = launchContextResult.data;
18773
+ const now = this.now();
18774
+ const thread = await this.resolveThread(meta, worktree.path, allowCreate, launchContext);
18775
+ if (!thread) {
18776
+ return err(404, "No Codex thread could be resolved for this worktree");
18777
+ }
18778
+ const conversationMeta = buildConversationMeta(thread, now);
18779
+ const nextMeta = sameConversationMeta(meta.conversation, conversationMeta) ? { ...meta, conversation: { ...conversationMeta, lastSeenAt: meta.conversation?.lastSeenAt ?? conversationMeta.lastSeenAt } } : { ...meta, conversation: conversationMeta };
18780
+ if (!sameConversationMeta(meta.conversation, conversationMeta)) {
18781
+ await this.writeMeta(gitDir, nextMeta);
18782
+ }
18783
+ return ok({
18784
+ gitDir,
18785
+ meta: nextMeta,
18786
+ thread,
18787
+ conversationMeta: nextMeta.conversation ?? conversationMeta,
18788
+ launchContext
18789
+ });
18790
+ }
18791
+ async resolveThread(meta, cwd, allowCreate, launchContext) {
18792
+ const savedThreadId = isCodexConversationMeta(meta.conversation) ? meta.conversation.threadId : null;
18793
+ if (savedThreadId) {
18794
+ const savedThread = await this.tryLoadThread(savedThreadId, cwd, launchContext);
18795
+ if (savedThread)
18796
+ return savedThread;
18797
+ log.warn(`[agents] saved codex thread missing, starting fresh conversation cwd=${cwd} threadId=${savedThreadId}`);
18798
+ } else {
18799
+ const discoveredThread = selectDiscoveredThread((await this.deps.appServer.threadList({
18800
+ cwd,
18801
+ limit: 20,
18802
+ sortKey: "updated_at"
18803
+ })).data);
18804
+ if (discoveredThread) {
18805
+ return await this.ensureThreadLoaded(discoveredThread.id, cwd, launchContext);
18806
+ }
18807
+ }
18808
+ if (!allowCreate)
18809
+ return null;
18810
+ const started = await this.deps.appServer.threadStart({
18811
+ cwd,
18812
+ approvalPolicy: launchContext.approvalPolicy,
18813
+ personality: launchContext.personality,
18814
+ sandbox: launchContext.sandbox
18815
+ });
18816
+ return started.thread;
18817
+ }
18818
+ async tryLoadThread(threadId, cwd, launchContext) {
18819
+ try {
18820
+ return await this.ensureThreadLoaded(threadId, cwd, launchContext);
18821
+ } catch {
18822
+ return null;
18823
+ }
18824
+ }
18825
+ async ensureThreadLoaded(threadId, cwd, launchContext) {
18826
+ const initial = await this.deps.appServer.threadRead(threadId, false);
18827
+ if (initial.thread.status.type === "notLoaded") {
18828
+ await this.deps.appServer.threadResume({
18829
+ threadId,
18830
+ cwd,
18831
+ approvalPolicy: launchContext.approvalPolicy,
18832
+ personality: launchContext.personality,
18833
+ sandbox: launchContext.sandbox
18834
+ });
18835
+ }
18836
+ return (await this.deps.appServer.threadRead(threadId, true)).thread;
18837
+ }
18838
+ }
18839
+
18840
+ // backend/src/services/agents-ui-stream-service.ts
18841
+ function readNotificationParams(raw) {
18842
+ return isRecord3(raw) ? raw : null;
18843
+ }
18844
+ function readThreadId(raw) {
18845
+ return typeof raw === "string" && raw.length > 0 ? raw : null;
18846
+ }
18847
+ function readStatusType(raw) {
18848
+ if (typeof raw === "string")
18849
+ return raw;
18850
+ if (!isRecord3(raw))
18851
+ return null;
18852
+ return typeof raw.type === "string" ? raw.type : null;
18853
+ }
18854
+ function readNotificationStatusType(notification) {
18855
+ const params = readNotificationParams(notification.params);
18856
+ if (!params)
18857
+ return null;
18858
+ return readStatusType(params.status) ?? (isRecord3(params.thread) ? readStatusType(params.thread.status) : null);
18859
+ }
18860
+ function isTerminalThreadStatus(statusType) {
18861
+ return statusType === "idle" || statusType === "completed" || statusType === "interrupted" || statusType === "failed" || statusType === "systemError";
18862
+ }
18863
+ function readNumber(raw) {
18864
+ return typeof raw === "number" ? raw : null;
18865
+ }
18866
+ function toIsoTimestampMs(epochMs) {
18867
+ if (epochMs === null)
18868
+ return null;
18869
+ return new Date(epochMs).toISOString();
18870
+ }
18871
+ function readNotificationItem(notification) {
18872
+ const params = readNotificationParams(notification.params);
18873
+ if (!params)
18874
+ return null;
18875
+ return parseCodexAppServerThreadItem(params.item);
18876
+ }
18877
+ function orderSpanForItem(item) {
18878
+ switch (item.type) {
18879
+ case "userMessage":
18880
+ case "agentMessage":
18881
+ case "webSearch":
18882
+ return 1;
18883
+ case "commandExecution":
18884
+ case "fileChange":
18885
+ case "mcpToolCall":
18886
+ case "dynamicToolCall":
18887
+ return 2;
18888
+ default:
18889
+ return null;
18890
+ }
18891
+ }
18892
+ function readAgentsNotificationThreadId(notification) {
18893
+ const params = readNotificationParams(notification.params);
18894
+ if (!params)
18895
+ return null;
18896
+ return readThreadId(params.threadId);
18897
+ }
18898
+ function buildAgentsUiMessageDeltaEvent(notification, order) {
18899
+ if (notification.method !== "item/agentMessage/delta")
18900
+ return null;
18901
+ const params = readNotificationParams(notification.params);
18902
+ if (!params)
18903
+ return null;
18904
+ const threadId = readThreadId(params.threadId);
18905
+ const turnId = readThreadId(params.turnId);
18906
+ const itemId = readThreadId(params.itemId);
18907
+ const delta = typeof params.delta === "string" ? params.delta : null;
18908
+ if (!threadId || !turnId || !itemId || delta === null)
18909
+ return null;
18910
+ return {
18911
+ type: "messageDelta",
18912
+ conversationId: threadId,
18913
+ turnId,
18914
+ itemId,
18915
+ order,
18916
+ delta
18917
+ };
18918
+ }
18919
+ function buildAgentsUiMessageUpsertEvents(notification, order) {
18920
+ if (notification.method !== "item/started" && notification.method !== "item/completed")
18921
+ return [];
18922
+ const params = readNotificationParams(notification.params);
18923
+ if (!params)
18924
+ return [];
18925
+ const threadId = readThreadId(params.threadId);
18926
+ const turnId = readThreadId(params.turnId);
18927
+ if (!threadId || !turnId)
18928
+ return [];
18929
+ const item = readNotificationItem(notification);
18930
+ if (!item)
18931
+ return [];
18932
+ const createdAt = toIsoTimestampMs(notification.method === "item/started" ? readNumber(params.startedAtMs) : readNumber(params.completedAtMs));
18933
+ return buildCodexItemConversationMessages({
18934
+ item,
18935
+ turnId,
18936
+ turnStatus: notification.method === "item/started" ? "inProgress" : "completed",
18937
+ createdAt,
18938
+ order,
18939
+ includeEmptyText: true
18940
+ }).map((message) => ({
18941
+ type: "messageUpsert",
18942
+ conversationId: threadId,
18943
+ message
18944
+ }));
18945
+ }
18946
+ function buildAgentsUiConversationStatusEvent(notification) {
18947
+ if (notification.method !== "turn/started" && notification.method !== "turn/completed" && notification.method !== "thread/status/changed")
18948
+ return null;
18949
+ const params = readNotificationParams(notification.params);
18950
+ if (!params)
18951
+ return null;
18952
+ const conversationId = readThreadId(params.threadId);
18953
+ if (!conversationId)
18954
+ return null;
18955
+ if (notification.method === "thread/status/changed") {
18956
+ if (!isTerminalThreadStatus(readNotificationStatusType(notification)))
18957
+ return null;
18958
+ return {
18959
+ type: "conversationStatus",
18960
+ conversationId,
18961
+ running: false,
18962
+ activeTurnId: null
18963
+ };
18964
+ }
18965
+ if (notification.method === "turn/started") {
18966
+ const activeTurnId = readThreadId(params.turnId);
18967
+ if (!activeTurnId)
18968
+ return null;
18969
+ return {
18970
+ type: "conversationStatus",
18971
+ conversationId,
18972
+ running: true,
18973
+ activeTurnId
18974
+ };
18975
+ }
18976
+ return {
18977
+ type: "conversationStatus",
18978
+ conversationId,
18979
+ running: false,
18980
+ activeTurnId: null
18981
+ };
18982
+ }
18983
+
18984
+ class AgentsConversationStreamSession {
18985
+ deps;
18986
+ revision = 0;
18987
+ conversationId;
18988
+ closed = false;
18989
+ nextLiveOrder;
18990
+ itemOrders = new Map;
18991
+ constructor(deps) {
18992
+ this.deps = deps;
18993
+ this.conversationId = deps.conversationId;
18994
+ this.nextLiveOrder = deps.nextOrder;
18995
+ }
18996
+ currentConversationId() {
18997
+ return this.conversationId;
18998
+ }
18999
+ close() {
19000
+ this.closed = true;
19001
+ }
19002
+ handleNotification(notification) {
19003
+ if (this.closed)
19004
+ return;
19005
+ const notificationThreadId = readAgentsNotificationThreadId(notification);
19006
+ if (!notificationThreadId || notificationThreadId !== this.conversationId)
19007
+ return;
19008
+ const statusEvent = buildAgentsUiConversationStatusEvent(notification);
19009
+ if (statusEvent) {
19010
+ this.deps.send({
19011
+ ...statusEvent,
19012
+ revision: this.nextRevision()
19013
+ });
19014
+ return;
19015
+ }
19016
+ const deltaOrder = this.orderForDeltaNotification(notification);
19017
+ const deltaEvent = deltaOrder === null ? null : buildAgentsUiMessageDeltaEvent(notification, deltaOrder);
19018
+ if (deltaEvent) {
19019
+ this.deps.send({
19020
+ ...deltaEvent,
19021
+ revision: this.nextRevision()
19022
+ });
19023
+ return;
19024
+ }
19025
+ const upsertOrder = this.orderForUpsertNotification(notification);
19026
+ if (upsertOrder !== null) {
19027
+ for (const upsertEvent of buildAgentsUiMessageUpsertEvents(notification, upsertOrder)) {
19028
+ this.deps.send({
19029
+ ...upsertEvent,
19030
+ revision: this.nextRevision()
19031
+ });
19032
+ }
19033
+ }
19034
+ }
19035
+ nextRevision() {
19036
+ this.revision += 1;
19037
+ return this.revision;
19038
+ }
19039
+ reserveOrder(itemId, span) {
19040
+ const existing = this.itemOrders.get(itemId);
19041
+ if (existing !== undefined)
19042
+ return existing;
19043
+ const order = this.nextLiveOrder;
19044
+ this.nextLiveOrder += span;
19045
+ this.itemOrders.set(itemId, order);
19046
+ return order;
19047
+ }
19048
+ orderForDeltaNotification(notification) {
19049
+ if (notification.method !== "item/agentMessage/delta")
19050
+ return null;
19051
+ const params = readNotificationParams(notification.params);
19052
+ if (!params)
19053
+ return null;
19054
+ const itemId = readThreadId(params.itemId);
19055
+ return itemId ? this.reserveOrder(itemId, 1) : null;
19056
+ }
19057
+ orderForUpsertNotification(notification) {
19058
+ if (notification.method !== "item/started" && notification.method !== "item/completed")
19059
+ return null;
19060
+ const params = readNotificationParams(notification.params);
19061
+ if (!params || !isRecord3(params.item))
19062
+ return null;
19063
+ const itemId = readThreadId(params.item.id);
19064
+ const item = readNotificationItem(notification);
19065
+ if (!itemId || !item)
19066
+ return null;
19067
+ const orderSpan = orderSpanForItem(item);
19068
+ return orderSpan === null ? null : this.reserveOrder(itemId, orderSpan);
19069
+ }
19070
+ }
19071
+
19072
+ // backend/src/services/agents-ui-action-service.ts
19073
+ function classifyAgentsTerminalWorktreeError(error) {
19074
+ const message = error instanceof Error ? error.message : String(error);
19075
+ if (message.startsWith("No open tmux window found for worktree: ")) {
19076
+ return { status: 409, error: message };
19077
+ }
19078
+ if (message.startsWith("Worktree not found: ")) {
19079
+ return { status: 404, error: message };
19080
+ }
19081
+ return null;
19082
+ }
19083
+
19084
+ // backend/src/services/snapshot-service.ts
19085
+ function formatElapsedSince(startedAt, now) {
19086
+ if (!startedAt)
19087
+ return "";
19088
+ const startedMs = Date.parse(startedAt);
19089
+ if (Number.isNaN(startedMs))
19090
+ return "";
19091
+ const diffMs = Math.max(0, now().getTime() - startedMs);
19092
+ const diffMinutes = Math.floor(diffMs / 60000);
19093
+ if (diffMinutes < 1)
19094
+ return "0m";
19095
+ if (diffMinutes < 60)
19096
+ return `${diffMinutes}m`;
19097
+ const diffHours = Math.floor(diffMinutes / 60);
19098
+ if (diffHours < 24)
19099
+ return `${diffHours}h`;
19100
+ const diffDays = Math.floor(diffHours / 24);
19101
+ return `${diffDays}d`;
19102
+ }
19103
+ function clonePrEntry(pr) {
19104
+ return {
19105
+ ...pr,
19106
+ ciChecks: pr.ciChecks.map((check) => ({ ...check })),
19107
+ comments: pr.comments.map((comment) => ({ ...comment }))
19108
+ };
19109
+ }
19110
+ function mapCreationSnapshot(creating) {
19111
+ return creating ? {
19112
+ phase: creating.phase
19113
+ } : null;
19114
+ }
19115
+ function mapWorktreeSnapshot(state, now, creating, isArchived, findLinearIssue, findAgentLabel) {
19116
+ return {
19117
+ branch: state.branch,
19118
+ label: state.label,
19119
+ ...state.baseBranch ? { baseBranch: state.baseBranch } : {},
19120
+ path: state.path,
19121
+ dir: state.path,
19122
+ archived: isArchived(state.path),
19123
+ profile: state.profile,
19124
+ agentName: state.agentName,
19125
+ agentLabel: findAgentLabel ? findAgentLabel(state.agentName) : state.agentName,
19126
+ agentTerminalStale: state.agentTerminalStale,
19127
+ mux: state.session.exists,
19128
+ dirty: state.git.dirty,
19129
+ unpushed: state.git.aheadCount > 0,
19130
+ paneCount: state.session.paneCount,
19131
+ status: creating ? "creating" : state.agent.lifecycle,
19132
+ elapsed: formatElapsedSince(state.agent.lastStartedAt, now),
19133
+ services: state.services.map((service) => ({ ...service })),
19134
+ prs: state.prs.map((pr) => clonePrEntry(pr)),
19135
+ linearIssue: findLinearIssue ? findLinearIssue(state.branch) : null,
19136
+ creation: mapCreationSnapshot(creating),
19137
+ source: state.source,
19138
+ oneshot: state.oneshot
19139
+ };
19140
+ }
19141
+ function mapCreatingWorktreeSnapshot(creating, isArchived, findLinearIssue, findAgentLabel) {
19142
+ return {
19143
+ branch: creating.branch,
19144
+ label: null,
19145
+ ...creating.baseBranch ? { baseBranch: creating.baseBranch } : {},
19146
+ path: creating.path,
19147
+ dir: creating.path,
19148
+ archived: isArchived(creating.path),
19149
+ profile: creating.profile,
19150
+ agentName: creating.agentName,
19151
+ agentLabel: findAgentLabel ? findAgentLabel(creating.agentName) : creating.agentName,
19152
+ agentTerminalStale: false,
19153
+ mux: false,
19154
+ dirty: false,
19155
+ unpushed: false,
19156
+ paneCount: 0,
19157
+ status: "creating",
19158
+ elapsed: "",
19159
+ services: [],
19160
+ prs: [],
19161
+ linearIssue: findLinearIssue ? findLinearIssue(creating.branch) : null,
19162
+ creation: mapCreationSnapshot(creating),
19163
+ source: creating.source,
19164
+ oneshot: null
19165
+ };
19166
+ }
19167
+ function buildWorktreeSnapshots(input) {
19168
+ const now = input.now ?? (() => new Date);
19169
+ const isArchived = input.isArchived ?? (() => false);
19170
+ const creatingWorktrees = input.creatingWorktrees ?? [];
19171
+ const creatingByBranch = new Map(creatingWorktrees.map((worktree) => [worktree.branch, worktree]));
19172
+ const runtimeWorktrees = input.runtime.listWorktrees();
19173
+ const runtimeBranches = new Set(runtimeWorktrees.map((worktree) => worktree.branch));
19174
+ const worktrees = runtimeWorktrees.map((state) => mapWorktreeSnapshot(state, now, creatingByBranch.get(state.branch) ?? null, isArchived, input.findLinearIssue, input.findAgentLabel));
19175
+ for (const creating of creatingWorktrees) {
19176
+ if (!runtimeBranches.has(creating.branch)) {
19177
+ worktrees.push(mapCreatingWorktreeSnapshot(creating, isArchived, input.findLinearIssue, input.findAgentLabel));
19178
+ }
19179
+ }
19180
+ worktrees.sort((left, right) => left.branch.localeCompare(right.branch));
19181
+ return worktrees;
19182
+ }
19183
+ function buildProjectSnapshot(input) {
19184
+ return {
19185
+ project: {
19186
+ name: input.projectName,
19187
+ mainBranch: input.mainBranch
19188
+ },
19189
+ worktrees: buildWorktreeSnapshots(input),
19190
+ notifications: input.notifications.map((notification) => ({ ...notification }))
19191
+ };
19192
+ }
19193
+
19194
+ // backend/src/services/claude-conversation-service.ts
19195
+ function isClaudeWorktree(worktree) {
19196
+ return worktree.agentName === "claude";
19197
+ }
19198
+ function isClaudeConversationMeta(meta) {
19199
+ return meta?.provider === "claudeCode";
19200
+ }
19201
+ function buildPendingConversationId(worktree) {
19202
+ return `claude-pending:${worktree.path}`;
19203
+ }
19204
+ function buildClaudeConversationMeta(sessionId, cwd, now) {
19205
+ return {
19206
+ provider: "claudeCode",
19207
+ conversationId: sessionId,
19208
+ sessionId,
19209
+ cwd,
19210
+ lastSeenAt: now.toISOString()
19211
+ };
19212
+ }
19213
+ function sameConversationMeta2(left, right) {
19214
+ return left?.provider === right.provider && left.conversationId === right.conversationId && left.cwd === right.cwd;
19215
+ }
19216
+ function normalizeSessionMessages(messages) {
19217
+ return messages.map((message, order) => ({
18012
19218
  ...message,
19219
+ order,
19220
+ kind: message.kind ?? "text",
18013
19221
  status: "completed"
18014
19222
  }));
18015
19223
  }
18016
- function buildConversationState(worktree, session) {
19224
+ function buildConversationState2(worktree, session) {
18017
19225
  return {
18018
19226
  provider: "claudeCode",
18019
19227
  conversationId: session?.sessionId ?? buildPendingConversationId(worktree),
@@ -18023,10 +19231,10 @@ function buildConversationState(worktree, session) {
18023
19231
  messages: normalizeSessionMessages(session?.messages ?? [])
18024
19232
  };
18025
19233
  }
18026
- function toWorktreeConversationResponse(worktree, conversationMeta, session) {
19234
+ function toWorktreeConversationResponse2(worktree, conversationMeta, session) {
18027
19235
  return {
18028
19236
  worktree: buildAgentsUiWorktreeSummary(worktree, conversationMeta),
18029
- conversation: buildConversationState(worktree, session)
19237
+ conversation: buildConversationState2(worktree, session)
18030
19238
  };
18031
19239
  }
18032
19240
 
@@ -18042,10 +19250,10 @@ class ClaudeConversationService {
18042
19250
  this.writeMeta = deps.writeMeta ?? writeWorktreeMeta;
18043
19251
  }
18044
19252
  async attachWorktreeConversation(worktree) {
18045
- return await this.withResolvedConversation(worktree, async (resolved) => ok(toWorktreeConversationResponse(worktree, resolved.conversationMeta, resolved.session)));
19253
+ return await this.withResolvedConversation(worktree, async (resolved) => ok(toWorktreeConversationResponse2(worktree, resolved.conversationMeta, resolved.session)));
18046
19254
  }
18047
19255
  async readWorktreeConversation(worktree) {
18048
- return await this.withResolvedConversation(worktree, async (resolved) => ok(toWorktreeConversationResponse(worktree, resolved.conversationMeta, resolved.session)));
19256
+ return await this.withResolvedConversation(worktree, async (resolved) => ok(toWorktreeConversationResponse2(worktree, resolved.conversationMeta, resolved.session)));
18049
19257
  }
18050
19258
  async withResolvedConversation(worktree, fn) {
18051
19259
  if (!isClaudeWorktree(worktree)) {
@@ -18094,7 +19302,7 @@ class ClaudeConversationService {
18094
19302
  }
18095
19303
  async persistConversationMeta(gitDir, meta, cwd, sessionId) {
18096
19304
  const nextConversation = buildClaudeConversationMeta(sessionId, cwd, this.now());
18097
- if (!sameConversationMeta(meta.conversation, nextConversation)) {
19305
+ if (!sameConversationMeta2(meta.conversation, nextConversation)) {
18098
19306
  await this.writeMeta(gitDir, {
18099
19307
  ...meta,
18100
19308
  conversation: nextConversation
@@ -18104,270 +19312,6 @@ class ClaudeConversationService {
18104
19312
  }
18105
19313
  }
18106
19314
 
18107
- // backend/src/services/worktree-conversation-service.ts
18108
- function resolveCodexAppServerLaunchContext(input) {
18109
- if (input.worktree.agentName !== "codex" || input.meta.agent !== "codex") {
18110
- return err(409, "Codex web chat is only available for Codex worktrees");
18111
- }
18112
- if (!input.profile) {
18113
- return err(409, `Profile is missing for Codex web chat: ${input.meta.profile}`);
18114
- }
18115
- if (input.meta.runtime !== "host" || input.profile.runtime !== "host") {
18116
- return err(409, "Codex web chat is only available for host-runtime worktrees. Use the terminal for Docker worktrees.");
18117
- }
18118
- if (input.profile.yolo !== true) {
18119
- return err(409, "Codex web chat requires a yolo profile to preserve the Codex approval policy. Use the terminal for this worktree.");
18120
- }
18121
- return ok({
18122
- approvalPolicy: "never",
18123
- personality: "pragmatic",
18124
- sandbox: "danger-full-access"
18125
- });
18126
- }
18127
- function isCodexWorktree(worktree) {
18128
- return worktree.agentName === "codex";
18129
- }
18130
- function isCodexConversationMeta(meta) {
18131
- return meta?.provider === "codexAppServer";
18132
- }
18133
- function toIsoTimestamp(epochSeconds) {
18134
- if (epochSeconds === null)
18135
- return null;
18136
- return new Date(epochSeconds * 1000).toISOString();
18137
- }
18138
- function isUserMessageItem(item) {
18139
- return item.type === "userMessage";
18140
- }
18141
- function isAgentMessageItem(item) {
18142
- return item.type === "agentMessage";
18143
- }
18144
- function extractUserText(item) {
18145
- return item.content.map((contentItem) => contentItem.text ?? "").join("").trim();
18146
- }
18147
- function extractAgentText(item) {
18148
- return item.text ?? item.message ?? "";
18149
- }
18150
- function isActiveTurnStatus(status) {
18151
- return status === "inProgress" || status === "active" || status === "running" || status === "pending" || status === "queued";
18152
- }
18153
- function findActiveTurn(thread) {
18154
- for (let index = thread.turns.length - 1;index >= 0; index -= 1) {
18155
- const turn = thread.turns[index];
18156
- if (isActiveTurnStatus(turn.status))
18157
- return turn;
18158
- }
18159
- return null;
18160
- }
18161
- function buildConversationMessages(thread) {
18162
- const messages = [];
18163
- for (const turn of thread.turns) {
18164
- for (const item of turn.items) {
18165
- if (isUserMessageItem(item)) {
18166
- const text2 = extractUserText(item);
18167
- if (text2.length === 0)
18168
- continue;
18169
- messages.push({
18170
- id: item.id,
18171
- turnId: turn.id,
18172
- role: "user",
18173
- text: text2,
18174
- status: "completed",
18175
- createdAt: toIsoTimestamp(turn.startedAt)
18176
- });
18177
- continue;
18178
- }
18179
- if (!isAgentMessageItem(item))
18180
- continue;
18181
- const text = extractAgentText(item);
18182
- if (text.length === 0)
18183
- continue;
18184
- messages.push({
18185
- id: item.id,
18186
- turnId: turn.id,
18187
- role: "assistant",
18188
- text,
18189
- status: isActiveTurnStatus(turn.status) ? "inProgress" : "completed",
18190
- createdAt: toIsoTimestamp(turn.completedAt ?? turn.startedAt)
18191
- });
18192
- }
18193
- }
18194
- return messages;
18195
- }
18196
- function buildConversationState2(thread) {
18197
- const activeTurn = findActiveTurn(thread);
18198
- return {
18199
- provider: "codexAppServer",
18200
- conversationId: thread.id,
18201
- cwd: thread.cwd,
18202
- running: thread.status.type === "active" || activeTurn !== null,
18203
- activeTurnId: activeTurn?.id ?? null,
18204
- messages: buildConversationMessages(thread)
18205
- };
18206
- }
18207
- function selectDiscoveredThread(threads) {
18208
- if (threads.length === 0)
18209
- return null;
18210
- return [...threads].sort((left, right) => right.updatedAt - left.updatedAt)[0] ?? null;
18211
- }
18212
- function buildConversationMeta(thread, now) {
18213
- return {
18214
- provider: "codexAppServer",
18215
- conversationId: thread.id,
18216
- threadId: thread.id,
18217
- cwd: thread.cwd,
18218
- lastSeenAt: now.toISOString()
18219
- };
18220
- }
18221
- function sameConversationMeta2(left, right) {
18222
- return left?.provider === right.provider && left.conversationId === right.conversationId && left.cwd === right.cwd;
18223
- }
18224
- function toWorktreeConversationResponse2(worktree, conversationMeta, thread) {
18225
- return {
18226
- worktree: buildAgentsUiWorktreeSummary(worktree, conversationMeta),
18227
- conversation: buildConversationState2(thread)
18228
- };
18229
- }
18230
-
18231
- class WorktreeConversationService {
18232
- deps;
18233
- now;
18234
- readMeta;
18235
- writeMeta;
18236
- constructor(deps) {
18237
- this.deps = deps;
18238
- this.now = deps.now ?? (() => new Date);
18239
- this.readMeta = deps.readMeta ?? readWorktreeMeta;
18240
- this.writeMeta = deps.writeMeta ?? writeWorktreeMeta;
18241
- }
18242
- async attachWorktreeConversation(worktree) {
18243
- return await this.withResolvedConversation(worktree, true, async ({ conversationMeta, thread }) => ok(toWorktreeConversationResponse2(worktree, conversationMeta, thread)));
18244
- }
18245
- async readWorktreeConversation(worktree) {
18246
- return await this.withResolvedConversation(worktree, false, async ({ conversationMeta, thread }) => ok(toWorktreeConversationResponse2(worktree, conversationMeta, thread)));
18247
- }
18248
- async sendWorktreeConversationMessage(worktree, text) {
18249
- return await this.withResolvedConversation(worktree, true, async ({ thread, launchContext }) => {
18250
- const started = await this.deps.appServer.turnStart({
18251
- threadId: thread.id,
18252
- cwd: worktree.path,
18253
- approvalPolicy: launchContext.approvalPolicy,
18254
- input: [{ type: "text", text }]
18255
- });
18256
- return ok({
18257
- conversationId: thread.id,
18258
- turnId: started.turn.id,
18259
- running: true
18260
- });
18261
- });
18262
- }
18263
- async interruptWorktreeConversation(worktree) {
18264
- return await this.withResolvedConversation(worktree, false, async ({ thread }) => {
18265
- const conversation = buildConversationState2(thread);
18266
- const turnId = conversation.activeTurnId;
18267
- if (!turnId) {
18268
- return err(409, "No active Codex turn to interrupt");
18269
- }
18270
- await this.deps.appServer.turnInterrupt({
18271
- threadId: thread.id,
18272
- turnId
18273
- });
18274
- return ok({
18275
- conversationId: thread.id,
18276
- turnId,
18277
- interrupted: true
18278
- });
18279
- });
18280
- }
18281
- async withResolvedConversation(worktree, allowCreate, fn) {
18282
- if (!isCodexWorktree(worktree)) {
18283
- return err(409, "Worktree chat is only available for Codex worktrees");
18284
- }
18285
- try {
18286
- const resolved = await this.resolveConversation(worktree, allowCreate);
18287
- if (!resolved.ok)
18288
- return resolved;
18289
- return await fn(resolved.data);
18290
- } catch (error) {
18291
- const message = error instanceof Error ? error.message : String(error);
18292
- return err(502, message);
18293
- }
18294
- }
18295
- async resolveConversation(worktree, allowCreate) {
18296
- const gitDir = this.deps.git.resolveWorktreeGitDir(worktree.path);
18297
- const meta = await this.readMeta(gitDir);
18298
- if (!meta) {
18299
- return err(409, "Worktree metadata is missing");
18300
- }
18301
- const launchContextResult = await this.deps.resolveLaunchContext({ worktree, meta });
18302
- if (!launchContextResult.ok)
18303
- return launchContextResult;
18304
- const launchContext = launchContextResult.data;
18305
- const now = this.now();
18306
- const thread = await this.resolveThread(meta, worktree.path, allowCreate, launchContext);
18307
- if (!thread) {
18308
- return err(404, "No Codex thread could be resolved for this worktree");
18309
- }
18310
- const conversationMeta = buildConversationMeta(thread, now);
18311
- const nextMeta = sameConversationMeta2(meta.conversation, conversationMeta) ? { ...meta, conversation: { ...conversationMeta, lastSeenAt: meta.conversation?.lastSeenAt ?? conversationMeta.lastSeenAt } } : { ...meta, conversation: conversationMeta };
18312
- if (!sameConversationMeta2(meta.conversation, conversationMeta)) {
18313
- await this.writeMeta(gitDir, nextMeta);
18314
- }
18315
- return ok({
18316
- gitDir,
18317
- meta: nextMeta,
18318
- thread,
18319
- conversationMeta: nextMeta.conversation ?? conversationMeta,
18320
- launchContext
18321
- });
18322
- }
18323
- async resolveThread(meta, cwd, allowCreate, launchContext) {
18324
- const discoveredThread = selectDiscoveredThread((await this.deps.appServer.threadList({
18325
- cwd,
18326
- limit: 20,
18327
- sortKey: "updated_at"
18328
- })).data);
18329
- if (discoveredThread) {
18330
- return await this.ensureThreadLoaded(discoveredThread.id, cwd, launchContext);
18331
- }
18332
- const savedThreadId = isCodexConversationMeta(meta.conversation) ? meta.conversation.threadId : null;
18333
- if (savedThreadId) {
18334
- const savedThread = await this.tryLoadThread(savedThreadId, cwd, launchContext);
18335
- if (savedThread)
18336
- return savedThread;
18337
- log.warn(`[agents] saved codex thread missing, rediscovering cwd=${cwd} threadId=${savedThreadId}`);
18338
- }
18339
- if (!allowCreate)
18340
- return null;
18341
- const started = await this.deps.appServer.threadStart({
18342
- cwd,
18343
- approvalPolicy: launchContext.approvalPolicy,
18344
- personality: launchContext.personality,
18345
- sandbox: launchContext.sandbox
18346
- });
18347
- return started.thread;
18348
- }
18349
- async tryLoadThread(threadId, cwd, launchContext) {
18350
- try {
18351
- return await this.ensureThreadLoaded(threadId, cwd, launchContext);
18352
- } catch {
18353
- return null;
18354
- }
18355
- }
18356
- async ensureThreadLoaded(threadId, cwd, launchContext) {
18357
- const initial = await this.deps.appServer.threadRead(threadId, false);
18358
- if (initial.thread.status.type === "notLoaded") {
18359
- await this.deps.appServer.threadResume({
18360
- threadId,
18361
- cwd,
18362
- approvalPolicy: launchContext.approvalPolicy,
18363
- personality: launchContext.personality,
18364
- sandbox: launchContext.sandbox
18365
- });
18366
- }
18367
- return (await this.deps.appServer.threadRead(threadId, true)).thread;
18368
- }
18369
- }
18370
-
18371
19315
  // backend/src/domain/events.ts
18372
19316
  function hasBaseFields(raw) {
18373
19317
  return typeof raw.worktreeId === "string" && raw.worktreeId.length > 0 && typeof raw.branch === "string" && raw.branch.length > 0 && typeof raw.type === "string" && ["agent_stopped", "agent_status_changed", "pr_opened", "runtime_error"].includes(raw.type);
@@ -20190,7 +21134,7 @@ async function apiRefreshWorktreeAgentTerminal(branch) {
20190
21134
  await lifecycleService.refreshAgentTerminal(branch);
20191
21135
  return jsonResponse({ ok: true });
20192
21136
  }
20193
- async function loadAgentsConversationSnapshot(branch) {
21137
+ async function loadAgentsConversationInitialState(branch) {
20194
21138
  const resolved = await resolveAgentsWorktree(branch);
20195
21139
  if (!resolved.ok) {
20196
21140
  return {
@@ -20221,45 +21165,55 @@ async function readErrorMessage(response) {
20221
21165
  const text = await response.text();
20222
21166
  return text.length > 0 ? text : `HTTP ${response.status}`;
20223
21167
  }
21168
+ function nextConversationMessageOrder(messages) {
21169
+ return messages.reduce((order, message) => Math.max(order, message.order + 1), 0);
21170
+ }
20224
21171
  async function openAgentsSocket(ws, data) {
20225
- const snapshot = await loadAgentsConversationSnapshot(data.branch);
20226
- if (!snapshot.ok) {
20227
- sendAgentsWs(ws, { type: "error", message: snapshot.message });
20228
- ws.close(1011, snapshot.message.slice(0, 123));
21172
+ let bufferingNotifications = true;
21173
+ let socketClosed = false;
21174
+ let streamSession = null;
21175
+ const bufferedNotifications = [];
21176
+ const unsubscribeNotifications = codexAppServerClient.onNotification((notification) => {
21177
+ if (bufferingNotifications || !streamSession) {
21178
+ bufferedNotifications.push(notification);
21179
+ return;
21180
+ }
21181
+ const notificationThreadId = readAgentsNotificationThreadId(notification);
21182
+ if (!notificationThreadId || notificationThreadId !== data.conversationId)
21183
+ return;
21184
+ streamSession.handleNotification(notification);
21185
+ data.conversationId = streamSession.currentConversationId();
21186
+ });
21187
+ data.unsubscribe = () => {
21188
+ socketClosed = true;
21189
+ streamSession?.close();
21190
+ unsubscribeNotifications();
21191
+ };
21192
+ const initialState = await loadAgentsConversationInitialState(data.branch);
21193
+ if (socketClosed)
21194
+ return;
21195
+ if (!initialState.ok) {
21196
+ unsubscribeNotifications();
21197
+ sendAgentsWs(ws, { type: "error", message: initialState.message });
21198
+ ws.close(1011, initialState.message.slice(0, 123));
20229
21199
  return;
20230
21200
  }
20231
- data.conversationId = snapshot.data.conversation.conversationId;
20232
- sendAgentsWs(ws, {
20233
- type: "snapshot",
20234
- data: snapshot.data
21201
+ streamSession = new AgentsConversationStreamSession({
21202
+ conversationId: initialState.data.conversation.conversationId,
21203
+ nextOrder: nextConversationMessageOrder(initialState.data.conversation.messages),
21204
+ send: (event) => sendAgentsWs(ws, event)
20235
21205
  });
20236
- if (snapshot.data.conversation.provider !== "codexAppServer") {
21206
+ data.conversationId = streamSession.currentConversationId();
21207
+ if (initialState.data.conversation.provider !== "codexAppServer") {
21208
+ unsubscribeNotifications();
21209
+ data.unsubscribe = null;
20237
21210
  return;
20238
21211
  }
20239
- data.unsubscribe = codexAppServerClient.onNotification((notification) => {
20240
- const notificationThreadId = readAgentsNotificationThreadId(notification);
20241
- if (!notificationThreadId || notificationThreadId !== data.conversationId)
20242
- return;
20243
- const deltaEvent = buildAgentsUiMessageDeltaEvent(notification);
20244
- if (deltaEvent) {
20245
- sendAgentsWs(ws, deltaEvent);
20246
- return;
20247
- }
20248
- if (!shouldRefreshAgentsConversationSnapshot(notification))
20249
- return;
20250
- (async () => {
20251
- const nextSnapshot = await loadAgentsConversationSnapshot(data.branch);
20252
- if (!nextSnapshot.ok) {
20253
- sendAgentsWs(ws, { type: "error", message: nextSnapshot.message });
20254
- return;
20255
- }
20256
- data.conversationId = nextSnapshot.data.conversation.conversationId;
20257
- sendAgentsWs(ws, {
20258
- type: "snapshot",
20259
- data: nextSnapshot.data
20260
- });
20261
- })();
20262
- });
21212
+ bufferingNotifications = false;
21213
+ for (const notification of bufferedNotifications) {
21214
+ streamSession.handleNotification(notification);
21215
+ data.conversationId = streamSession.currentConversationId();
21216
+ }
20263
21217
  }
20264
21218
  async function apiRuntimeEvent(req) {
20265
21219
  if (!await hasValidControlToken(req)) {