remote-codex 0.1.5 → 0.1.6

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.
@@ -248,6 +248,49 @@ function mapMcpServer(record) {
248
248
  resourceTemplateCount: Array.isArray(record.resourceTemplates) ? record.resourceTemplates.length : 0
249
249
  };
250
250
  }
251
+ function numberFromProtocolInteger(value, fallback = 0) {
252
+ if (typeof value === "number" && Number.isFinite(value)) {
253
+ return value;
254
+ }
255
+ if (typeof value === "bigint") {
256
+ return Number(value);
257
+ }
258
+ if (typeof value === "string") {
259
+ const numericValue = Number(value);
260
+ return Number.isFinite(numericValue) ? numericValue : fallback;
261
+ }
262
+ return fallback;
263
+ }
264
+ function mapHook(record) {
265
+ return {
266
+ key: record.key,
267
+ eventName: record.eventName,
268
+ handlerType: record.handlerType,
269
+ matcher: record.matcher ?? null,
270
+ command: record.command ?? null,
271
+ timeoutSec: numberFromProtocolInteger(record.timeoutSec ?? record.timeout_sec, 600),
272
+ statusMessage: record.statusMessage ?? record.status_message ?? null,
273
+ sourcePath: String(record.sourcePath ?? record.source_path ?? ""),
274
+ source: record.source ?? "unknown",
275
+ pluginId: record.pluginId ?? record.plugin_id ?? null,
276
+ displayOrder: numberFromProtocolInteger(record.displayOrder ?? record.display_order),
277
+ enabled: record.enabled === true,
278
+ isManaged: record.isManaged === true || record.is_managed === true,
279
+ currentHash: record.currentHash ?? record.current_hash ?? "",
280
+ trustStatus: record.trustStatus ?? record.trust_status ?? "untrusted"
281
+ };
282
+ }
283
+ function mapHooksListEntry(record) {
284
+ return {
285
+ cwd: record.cwd,
286
+ hooks: Array.isArray(record.hooks) ? record.hooks.map(mapHook) : [],
287
+ warnings: Array.isArray(record.warnings) ? record.warnings.map(String) : [],
288
+ errors: Array.isArray(record.errors) ? record.errors.map((error) => ({
289
+ path: String(error.path ?? ""),
290
+ message: String(error.message ?? "")
291
+ })) : []
292
+ };
293
+ }
251
294
  function parseGoalTimestamp(value) {
252
295
  if (typeof value === "number" && Number.isFinite(value)) {
253
296
  return value;
@@ -387,6 +430,31 @@ var CodexAppServerManager = class extends EventEmitter2 {
387
430
  } while (cursor);
388
431
  return servers;
389
432
  }
433
+ async listHooks(input = {}) {
434
+ await this.ensureReady();
435
+ const response = await this.client.request("hooks/list", {
436
+ ...input.cwds && input.cwds.length > 0 ? { cwds: input.cwds } : {}
437
+ });
438
+ return response.data.map(mapHooksListEntry);
439
+ }
440
+ async setHookTrust(input) {
441
+ await this.ensureReady();
442
+ await this.client.request("config/batchWrite", {
443
+ edits: [
444
+ {
445
+ keyPath: "hooks.state",
446
+ value: {
447
+ [input.key]: {
448
+ trusted_hash: input.trustedHash ?? "",
449
+ ...input.trustedHash ? { enabled: true } : {}
450
+ }
451
+ },
452
+ mergeStrategy: "upsert"
453
+ }
454
+ ],
455
+ reloadUserConfig: true
456
+ });
457
+ }
390
458
  async startThread(input) {
391
459
  await this.ensureReady();
392
460
  const response = await this.client.request("thread/start", {
@@ -5913,6 +5981,7 @@ __export(schema_exports, {
5913
5981
  threadActivityNotes: () => threadActivityNotes,
5914
5982
  threadForks: () => threadForks,
5915
5983
  threadGoals: () => threadGoals,
5984
+ threadHistoryItems: () => threadHistoryItems,
5916
5985
  threadPendingSteers: () => threadPendingSteers,
5917
5986
  threadTurnMetadata: () => threadTurnMetadata,
5918
5987
  threads: () => threads,
@@ -6025,6 +6094,25 @@ var threadPendingSteers = sqliteTable("thread_pending_steers", {
6025
6094
  createdAt: text("created_at").notNull(),
6026
6095
  updatedAt: text("updated_at").notNull()
6027
6096
  });
6097
+ var threadHistoryItems = sqliteTable(
6098
+ "thread_history_items",
6099
+ {
6100
+ id: text("id").primaryKey(),
6101
+ threadId: text("thread_id").notNull(),
6102
+ turnId: text("turn_id").notNull(),
6103
+ itemId: text("item_id").notNull(),
6104
+ itemJson: text("item_json").notNull(),
6105
+ createdAt: text("created_at").notNull(),
6106
+ updatedAt: text("updated_at").notNull()
6107
+ },
6108
+ (table) => ({
6109
+ threadTurnItemUnique: uniqueIndex("thread_history_items_thread_turn_item_idx").on(
6110
+ table.threadId,
6111
+ table.turnId,
6112
+ table.itemId
6113
+ )
6114
+ })
6115
+ );
6028
6116
  var threadActivityNotes = sqliteTable("thread_activity_notes", {
6029
6117
  id: text("id").primaryKey(),
6030
6118
  threadId: text("thread_id").notNull(),
@@ -6303,6 +6391,38 @@ function upsertThreadTurnMetadata(db, input) {
6303
6391
  function deleteThreadTurnMetadataByThreadId(db, threadId) {
6304
6392
  db.delete(threadTurnMetadata).where(eq(threadTurnMetadata.threadId, threadId)).run();
6305
6393
  }
6394
+ function listThreadHistoryItemRecordsByThreadId(db, threadId) {
6395
+ return db.select().from(threadHistoryItems).where(eq(threadHistoryItems.threadId, threadId)).orderBy(threadHistoryItems.createdAt).all();
6396
+ }
6397
+ function upsertThreadHistoryItemRecord(db, input) {
6398
+ const now = (/* @__PURE__ */ new Date()).toISOString();
6399
+ const existing = db.select().from(threadHistoryItems).where(
6400
+ and(
6401
+ eq(threadHistoryItems.threadId, input.threadId),
6402
+ eq(threadHistoryItems.turnId, input.turnId),
6403
+ eq(threadHistoryItems.itemId, input.itemId)
6404
+ )
6405
+ ).get();
6406
+ if (existing) {
6407
+ db.update(threadHistoryItems).set({
6408
+ itemJson: input.itemJson,
6409
+ updatedAt: now
6410
+ }).where(eq(threadHistoryItems.id, existing.id)).run();
6411
+ return;
6412
+ }
6413
+ db.insert(threadHistoryItems).values({
6414
+ id: randomUUID(),
6415
+ threadId: input.threadId,
6416
+ turnId: input.turnId,
6417
+ itemId: input.itemId,
6418
+ itemJson: input.itemJson,
6419
+ createdAt: now,
6420
+ updatedAt: now
6421
+ }).run();
6422
+ }
6423
+ function deleteThreadHistoryItemRecordsByThreadId(db, threadId) {
6424
+ db.delete(threadHistoryItems).where(eq(threadHistoryItems.threadId, threadId)).run();
6425
+ }
6306
6426
  function listThreadPendingSteerRecordsByThreadId(db, threadId) {
6307
6427
  return db.select().from(threadPendingSteers).where(eq(threadPendingSteers.threadId, threadId)).orderBy(threadPendingSteers.createdAt).all();
6308
6428
  }
@@ -8439,6 +8559,19 @@ var DEFERRED_TOOL_DETAIL_TITLE = "Tool Call Details";
8439
8559
  var CONTEXT_BASELINE_TOKENS = 12e3;
8440
8560
  var FAST_MODE_NOTE_ON = "Fast mode on";
8441
8561
  var FAST_MODE_NOTE_OFF = "Fast mode off";
8562
+ var HOOK_EVENT_JSON_KEYS = {
8563
+ preToolUse: "PreToolUse",
8564
+ permissionRequest: "PermissionRequest",
8565
+ postToolUse: "PostToolUse",
8566
+ preCompact: "PreCompact",
8567
+ postCompact: "PostCompact",
8568
+ sessionStart: "SessionStart",
8569
+ userPromptSubmit: "UserPromptSubmit",
8570
+ stop: "Stop"
8571
+ };
8572
+ var HOOK_EVENT_DTO_KEYS = Object.fromEntries(
8573
+ Object.entries(HOOK_EVENT_JSON_KEYS).map(([dtoKey, jsonKey]) => [jsonKey, dtoKey])
8574
+ );
8442
8575
  var GOAL_FEATURE_DISABLED_MESSAGE = "Codex /goal is experimental. Enable it by adding `goals = true` under `[features]` in ~/.codex/config.toml, then restart the Codex app-server.";
8443
8576
  function toIsoFromEpoch(value) {
8444
8577
  if (typeof value !== "number" || !Number.isFinite(value)) {
@@ -8606,6 +8739,14 @@ function isRemoteThreadBootstrapError(error) {
8606
8739
  }
8607
8740
  return error.message.includes("includeTurns is unavailable before first user message") || error.message.includes("is not materialized yet") || error.message.includes("no rollout found for thread id") || error.message.includes("failed to load rollout") || error.message.includes("rollout at") && error.message.includes("is empty");
8608
8741
  }
8742
+ function isUnsupportedHooksListError(error) {
8743
+ if (!(error instanceof JsonRpcClientError) || error.code !== "remote_error") {
8744
+ return false;
8745
+ }
8746
+ const remoteCode = error.details?.code;
8747
+ const message = error.message.toLowerCase();
8748
+ return remoteCode === -32601 || message.includes("endpoint not found") || message.includes("method not found") || message.includes("hooks/list") && message.includes("not found");
8749
+ }
8609
8750
  function parseTurnSteerRace(error) {
8610
8751
  if (!(error instanceof JsonRpcClientError) || error.code !== "remote_error") {
8611
8752
  return null;
@@ -8962,6 +9103,39 @@ function summarizeCommandText(text2) {
8962
9103
  const lines = normalizeTextLines(text2);
8963
9104
  return lines.find((line) => line.trim().length > 0) ?? lines[0] ?? "Command output";
8964
9105
  }
9106
+ function decodeXmlEntities(value) {
9107
+ return value.replace(/&quot;/g, '"').replace(/&apos;/g, "'").replace(/&gt;/g, ">").replace(/&lt;/g, "<").replace(/&amp;/g, "&");
9108
+ }
9109
+ function textFromUnknown(value) {
9110
+ if (typeof value === "string") {
9111
+ return value.trim() ? value : null;
9112
+ }
9113
+ if (Array.isArray(value)) {
9114
+ const parts = value.map((entry) => textFromUnknown(entry)).filter((entry) => Boolean(entry));
9115
+ return parts.length > 0 ? parts.join(" ") : null;
9116
+ }
9117
+ return null;
9118
+ }
9119
+ function parseHookPromptText(text2) {
9120
+ const match = text2.trim().match(/^<hook_prompt(?:\s+hook_run_id="([^"]+)")?>([\s\S]*)<\/hook_prompt>$/);
9121
+ if (!match) {
9122
+ return null;
9123
+ }
9124
+ const hookRunId = match[1] ? decodeXmlEntities(match[1]) : null;
9125
+ const output = decodeXmlEntities(match[2] ?? "").trim();
9126
+ const eventName = hookRunId?.split(":")[0] ?? "hook";
9127
+ const eventLabel = hookEventLabel(eventName);
9128
+ const sourcePath = hookRunId?.split(":").slice(2).join(":") || null;
9129
+ const outputEntries = output ? [{ kind: "warning", text: output }] : [];
9130
+ return {
9131
+ hookRunId,
9132
+ output,
9133
+ outputEntries,
9134
+ eventName,
9135
+ eventLabel,
9136
+ sourcePath
9137
+ };
9138
+ }
8965
9139
  function safeJsonStringify(value) {
8966
9140
  try {
8967
9141
  const serialized = JSON.stringify(value, null, 2);
@@ -8989,6 +9163,32 @@ function deferCommandHistoryItem(item, deferredDetails) {
8989
9163
  hasDeferredDetail: true
8990
9164
  };
8991
9165
  }
9166
+ function formatCommandHistoryItem(item, deferredDetails) {
9167
+ const nestedRecords = [
9168
+ item,
9169
+ isRecord(item.action) ? item.action : null,
9170
+ isRecord(item.result) ? item.result : null
9171
+ ].filter((candidate) => Boolean(candidate));
9172
+ const commandText = textFromUnknown(valueFromNestedRecords(nestedRecords, ["command", "cmd", "argv"])) ?? stringOrNull(item.text) ?? "Command output";
9173
+ const outputText = textFromUnknown(
9174
+ valueFromNestedRecords(nestedRecords, [
9175
+ "aggregatedOutput",
9176
+ "aggregated_output",
9177
+ "output",
9178
+ "stdout",
9179
+ "stderr",
9180
+ "text"
9181
+ ])
9182
+ ) ?? null;
9183
+ const detailText = [commandText, outputText].filter(Boolean).join("\n\n");
9184
+ const historyItem = {
9185
+ id: item.id,
9186
+ kind: "commandExecution",
9187
+ text: detailText,
9188
+ status: item.status ?? null
9189
+ };
9190
+ return deferredDetails ? deferCommandHistoryItem(historyItem, deferredDetails) : historyItem;
9191
+ }
8992
9192
  function deferToolCallHistoryItem(item, deferredDetails) {
8993
9193
  const fullText = item.detailText?.trim() || item.text || "Tool call";
8994
9194
  deferredDetails.set(item.id, {
@@ -9102,6 +9302,129 @@ function deferLargeHistoryItemDetails(turn, deferredDetails) {
9102
9302
  )
9103
9303
  };
9104
9304
  }
9305
+ function shouldPersistLiveHistoryItem(item) {
9306
+ return item.kind === "commandExecution" || item.kind === "fileChange" || item.kind === "hook" || item.kind === "toolCall" || item.kind === "webSearch";
9307
+ }
9308
+ function parseStoredHistoryItem(value) {
9309
+ try {
9310
+ const parsed = JSON.parse(value);
9311
+ if (parsed && typeof parsed === "object" && typeof parsed.id === "string" && typeof parsed.kind === "string" && typeof parsed.text === "string") {
9312
+ return parsed;
9313
+ }
9314
+ } catch {
9315
+ return null;
9316
+ }
9317
+ return null;
9318
+ }
9319
+ function hasHistoryItemSequence(item) {
9320
+ return typeof item.sequence === "number" && Number.isFinite(item.sequence);
9321
+ }
9322
+ function historyItemSequence(item) {
9323
+ return hasHistoryItemSequence(item) ? item.sequence : Number.POSITIVE_INFINITY;
9324
+ }
9325
+ function sortHistoryItemsBySequence(items) {
9326
+ if (!items.some(hasHistoryItemSequence)) {
9327
+ return items;
9328
+ }
9329
+ return items.map((item, index) => ({ item, index })).sort((left, right) => {
9330
+ const sequenceDelta = historyItemSequence(left.item) - historyItemSequence(right.item);
9331
+ return sequenceDelta === 0 ? left.index - right.index : sequenceDelta;
9332
+ }).map((entry) => entry.item);
9333
+ }
9334
+ function sortTurnItemsByRecordedSequence(items) {
9335
+ const leadingItems = [];
9336
+ let index = 0;
9337
+ while (index < items.length && items[index]?.kind === "userMessage" && !hasHistoryItemSequence(items[index])) {
9338
+ leadingItems.push(items[index]);
9339
+ index += 1;
9340
+ }
9341
+ return [...leadingItems, ...sortHistoryItemsBySequence(items.slice(index))];
9342
+ }
9343
+ function mergeHistoryItemsBySequence(items, missingItems) {
9344
+ if (missingItems.length === 0) {
9345
+ return items;
9346
+ }
9347
+ if (!missingItems.some(hasHistoryItemSequence)) {
9348
+ return [...items, ...missingItems];
9349
+ }
9350
+ const mergedItems = [...items];
9351
+ const orderedMissingItems = sortHistoryItemsBySequence(missingItems);
9352
+ for (const missingItem of orderedMissingItems) {
9353
+ if (!hasHistoryItemSequence(missingItem)) {
9354
+ mergedItems.push(missingItem);
9355
+ continue;
9356
+ }
9357
+ const firstGreaterIndex = mergedItems.findIndex(
9358
+ (item) => hasHistoryItemSequence(item) && historyItemSequence(item) > historyItemSequence(missingItem)
9359
+ );
9360
+ if (firstGreaterIndex >= 0) {
9361
+ mergedItems.splice(firstGreaterIndex, 0, missingItem);
9362
+ continue;
9363
+ }
9364
+ const lastLowerIndex = mergedItems.findLastIndex(
9365
+ (item) => hasHistoryItemSequence(item) && historyItemSequence(item) < historyItemSequence(missingItem)
9366
+ );
9367
+ if (lastLowerIndex >= 0) {
9368
+ mergedItems.splice(lastLowerIndex + 1, 0, missingItem);
9369
+ continue;
9370
+ }
9371
+ mergedItems.push(missingItem);
9372
+ }
9373
+ return mergedItems;
9374
+ }
9375
+ function mergePersistedHistoryItemsIntoTurns(turns, persistedItemsByTurnId, deferredDetails) {
9376
+ if (persistedItemsByTurnId.size === 0) {
9377
+ return turns;
9378
+ }
9379
+ return turns.map((turn) => {
9380
+ if (turn.status === "inProgress") {
9381
+ return turn;
9382
+ }
9383
+ const persistedItems = persistedItemsByTurnId.get(turn.id);
9384
+ if (!persistedItems || persistedItems.length === 0) {
9385
+ return turn;
9386
+ }
9387
+ let changed = false;
9388
+ const persistedItemsById = new Map(persistedItems.map((item) => [item.id, item]));
9389
+ const nextItems = turn.items.map((item) => {
9390
+ const persistedItem = persistedItemsById.get(item.id);
9391
+ if (!persistedItem || item.kind !== persistedItem.kind || persistedItem.kind !== "commandExecution" && persistedItem.kind !== "toolCall") {
9392
+ return item;
9393
+ }
9394
+ const existingText = item.detailText?.trim() || item.text.trim();
9395
+ const persistedText = persistedItem.detailText?.trim() || persistedItem.text.trim();
9396
+ if (persistedText.length <= existingText.length) {
9397
+ return item;
9398
+ }
9399
+ changed = true;
9400
+ persistedItemsById.delete(item.id);
9401
+ return persistedItem.kind === "commandExecution" ? deferCommandHistoryItem(
9402
+ persistedItem,
9403
+ deferredDetails
9404
+ ) : deferToolCallHistoryItem(
9405
+ persistedItem,
9406
+ deferredDetails
9407
+ );
9408
+ });
9409
+ const existingItemIds = new Set(nextItems.map((item) => item.id));
9410
+ const missingItems = [...persistedItemsById.values()].filter((item) => !existingItemIds.has(item.id)).map(
9411
+ (item) => item.kind === "commandExecution" ? deferCommandHistoryItem(
9412
+ item,
9413
+ deferredDetails
9414
+ ) : item.kind === "toolCall" ? deferToolCallHistoryItem(
9415
+ item,
9416
+ deferredDetails
9417
+ ) : item
9418
+ );
9419
+ if (missingItems.length === 0 && !changed) {
9420
+ return turn;
9421
+ }
9422
+ return {
9423
+ ...turn,
9424
+ items: mergeHistoryItemsBySequence(nextItems, missingItems)
9425
+ };
9426
+ });
9427
+ }
9105
9428
  function sanitizeAttachmentFileName(originalName) {
9106
9429
  const basename = path8.basename(originalName).trim() || "attachment";
9107
9430
  const extension = path8.extname(basename).replace(/[^a-zA-Z0-9.]/g, "");
@@ -9473,6 +9796,25 @@ function formatFileChangeHistoryItem(item) {
9473
9796
  };
9474
9797
  }
9475
9798
  function itemToHistoryItem(item, deferredDetails) {
9799
+ const hookPrompt = parseHookPromptText(codexItemText(item));
9800
+ if (hookPrompt) {
9801
+ return {
9802
+ id: `hook-prompt:${hookPrompt.hookRunId ?? item.id}`,
9803
+ kind: "hook",
9804
+ text: `${hookPrompt.eventLabel} hook`,
9805
+ previewText: hookPrompt.output || `${hookPrompt.eventLabel} hook`,
9806
+ detailText: hookPrompt.output || null,
9807
+ status: "Completed",
9808
+ hookEventName: hookPrompt.eventName,
9809
+ hookEventLabel: hookPrompt.eventLabel,
9810
+ hookHandlerType: "command",
9811
+ hookScope: "turn",
9812
+ hookSource: hookPrompt.sourcePath ? "project" : null,
9813
+ hookSourcePath: hookPrompt.sourcePath,
9814
+ hookStatusMessage: null,
9815
+ hookOutputEntries: hookPrompt.outputEntries
9816
+ };
9817
+ }
9476
9818
  switch (item.type) {
9477
9819
  case "userMessage":
9478
9820
  return {
@@ -9508,20 +9850,7 @@ function itemToHistoryItem(item, deferredDetails) {
9508
9850
  text: [item.summary?.join("\n") ?? "", item.text ?? ""].filter(Boolean).join("\n\n")
9509
9851
  };
9510
9852
  case "commandExecution":
9511
- return deferredDetails ? deferCommandHistoryItem(
9512
- {
9513
- id: item.id,
9514
- kind: "commandExecution",
9515
- text: [item.command ?? "", item.aggregatedOutput ?? ""].filter(Boolean).join("\n\n"),
9516
- status: item.status ?? null
9517
- },
9518
- deferredDetails
9519
- ) : {
9520
- id: item.id,
9521
- kind: "commandExecution",
9522
- text: [item.command ?? "", item.aggregatedOutput ?? ""].filter(Boolean).join("\n\n"),
9523
- status: item.status ?? null
9524
- };
9853
+ return formatCommandHistoryItem(item, deferredDetails);
9525
9854
  case "webSearch":
9526
9855
  case "web_search":
9527
9856
  case "webSearchCall":
@@ -9547,9 +9876,8 @@ function itemToHistoryItem(item, deferredDetails) {
9547
9876
  }
9548
9877
  }
9549
9878
  function liveCodexItemToHistoryItem(item, phase) {
9550
- const deferredDetails = /* @__PURE__ */ new Map();
9551
- const historyItem = itemToHistoryItem(item, deferredDetails);
9552
- if (historyItem.kind !== "commandExecution" && historyItem.kind !== "toolCall") {
9879
+ const historyItem = itemToHistoryItem(item);
9880
+ if (historyItem.kind !== "commandExecution" && historyItem.kind !== "toolCall" && historyItem.kind !== "fileChange" && historyItem.kind !== "webSearch") {
9553
9881
  return null;
9554
9882
  }
9555
9883
  return {
@@ -9557,6 +9885,373 @@ function liveCodexItemToHistoryItem(item, phase) {
9557
9885
  status: historyItem.status ?? (phase === "started" ? "running" : "completed")
9558
9886
  };
9559
9887
  }
9888
+ function hookEventLabel(value) {
9889
+ switch (value) {
9890
+ case "preToolUse":
9891
+ return "PreToolUse";
9892
+ case "permissionRequest":
9893
+ return "PermissionRequest";
9894
+ case "postToolUse":
9895
+ return "PostToolUse";
9896
+ case "preCompact":
9897
+ return "PreCompact";
9898
+ case "postCompact":
9899
+ return "PostCompact";
9900
+ case "sessionStart":
9901
+ return "SessionStart";
9902
+ case "userPromptSubmit":
9903
+ return "UserPromptSubmit";
9904
+ case "stop":
9905
+ return "Stop";
9906
+ default:
9907
+ return value;
9908
+ }
9909
+ }
9910
+ function hookStatusLabel(value) {
9911
+ switch (value) {
9912
+ case "running":
9913
+ return "Running";
9914
+ case "completed":
9915
+ return "Completed";
9916
+ case "failed":
9917
+ return "Failed";
9918
+ case "blocked":
9919
+ return "Blocked";
9920
+ case "stopped":
9921
+ return "Stopped";
9922
+ default:
9923
+ return value;
9924
+ }
9925
+ }
9926
+ function hookRunOutputEntryText(entry) {
9927
+ if (!isRecord(entry)) {
9928
+ return textFromUnknown(entry);
9929
+ }
9930
+ return textFromUnknown(entry.text) ?? textFromUnknown(entry.message) ?? textFromUnknown(entry.systemMessage) ?? textFromUnknown(entry.stopReason) ?? textFromUnknown(entry.reason) ?? textFromUnknown(entry.output) ?? textFromUnknown(entry.stdout) ?? textFromUnknown(entry.stderr);
9931
+ }
9932
+ function normalizeHookRunOutputEntries(run) {
9933
+ const rawEntries = Array.isArray(run.entries) ? run.entries : Array.isArray(run.outputEntries) ? run.outputEntries : Array.isArray(run.output_entries) ? run.output_entries : [];
9934
+ const entries = rawEntries.map((entry) => {
9935
+ const text2 = hookRunOutputEntryText(entry)?.trim();
9936
+ if (!text2) {
9937
+ return null;
9938
+ }
9939
+ return {
9940
+ kind: typeof entry.kind === "string" && entry.kind.trim() ? entry.kind : "context",
9941
+ text: text2
9942
+ };
9943
+ }).filter((entry) => Boolean(entry));
9944
+ const seenTexts = new Set(entries.map((entry) => entry.text));
9945
+ for (const [kind, value] of [
9946
+ ["context", run.output],
9947
+ ["context", run.text],
9948
+ ["warning", run.systemMessage],
9949
+ ["warning", run.stopReason],
9950
+ ["warning", run.reason],
9951
+ ["context", run.stdout],
9952
+ ["warning", run.stderr]
9953
+ ]) {
9954
+ const text2 = textFromUnknown(value)?.trim();
9955
+ if (text2 && !seenTexts.has(text2)) {
9956
+ entries.push({ kind, text: text2 });
9957
+ seenTexts.add(text2);
9958
+ }
9959
+ }
9960
+ return entries;
9961
+ }
9962
+ function hookRunToHistoryItem(run) {
9963
+ const eventLabel = hookEventLabel(run.eventName);
9964
+ const outputEntries = normalizeHookRunOutputEntries(run);
9965
+ const entryPreview = outputEntries.map((entry) => entry.text.trim()).filter(Boolean).join("\n").trim();
9966
+ const firstEntryLine = entryPreview.split("\n").find(Boolean) ?? null;
9967
+ const detailLines = [
9968
+ `Event: ${eventLabel}`,
9969
+ `Status: ${hookStatusLabel(run.status)}`,
9970
+ `Handler: ${run.handlerType}`,
9971
+ `Scope: ${run.scope}`,
9972
+ `Source: ${run.source}`,
9973
+ `Path: ${run.sourcePath}`,
9974
+ run.durationMs !== null ? `Duration: ${run.durationMs} ms` : null,
9975
+ run.statusMessage ? `Message: ${run.statusMessage}` : null,
9976
+ entryPreview ? `
9977
+ ${entryPreview}` : null
9978
+ ].filter((line) => Boolean(line));
9979
+ return {
9980
+ id: `hook:${run.id}`,
9981
+ kind: "hook",
9982
+ text: `${eventLabel} hook`,
9983
+ previewText: run.statusMessage ?? firstEntryLine ?? `${eventLabel} hook`,
9984
+ detailText: detailLines.join("\n"),
9985
+ status: hookStatusLabel(run.status),
9986
+ hookEventName: run.eventName,
9987
+ hookEventLabel: eventLabel,
9988
+ hookHandlerType: run.handlerType,
9989
+ hookScope: run.scope,
9990
+ hookSource: run.source,
9991
+ hookSourcePath: run.sourcePath,
9992
+ hookStatusMessage: run.statusMessage,
9993
+ hookOutputEntries: outputEntries
9994
+ };
9995
+ }
9996
+ function normalizeHooksJson(value) {
9997
+ if (!isRecord(value) || !isRecord(value.hooks)) {
9998
+ return { hooks: {} };
9999
+ }
10000
+ const hooks = {};
10001
+ for (const [eventName, groups] of Object.entries(value.hooks)) {
10002
+ hooks[eventName] = Array.isArray(groups) ? groups : [];
10003
+ }
10004
+ return { ...value, hooks };
10005
+ }
10006
+ function readJsonFileOrDefault(filePath) {
10007
+ return fs8.readFile(filePath, "utf8").then((raw) => {
10008
+ if (!raw.trim()) {
10009
+ return { hooks: {} };
10010
+ }
10011
+ return normalizeHooksJson(JSON.parse(raw));
10012
+ }).catch((error) => {
10013
+ if (error.code === "ENOENT") {
10014
+ return { hooks: {} };
10015
+ }
10016
+ throw error;
10017
+ });
10018
+ }
10019
+ function validateHookInput(input) {
10020
+ if (!HOOK_EVENT_JSON_KEYS[input.eventName]) {
10021
+ throw new HttpError(400, {
10022
+ code: "bad_request",
10023
+ message: "Unsupported hook event."
10024
+ });
10025
+ }
10026
+ if (input.scope !== "global" && input.scope !== "project") {
10027
+ throw new HttpError(400, {
10028
+ code: "bad_request",
10029
+ message: "Hook scope must be global or project."
10030
+ });
10031
+ }
10032
+ if (!input.command.trim()) {
10033
+ throw new HttpError(400, {
10034
+ code: "bad_request",
10035
+ message: "Hook command cannot be empty."
10036
+ });
10037
+ }
10038
+ if (input.timeoutSec !== void 0 && input.timeoutSec !== null && (!Number.isInteger(input.timeoutSec) || input.timeoutSec <= 0 || input.timeoutSec > 86400)) {
10039
+ throw new HttpError(400, {
10040
+ code: "bad_request",
10041
+ message: "Hook timeout must be a positive number of seconds."
10042
+ });
10043
+ }
10044
+ }
10045
+ async function writeHookJsonEntry({
10046
+ codexHome,
10047
+ workspacePath,
10048
+ input
10049
+ }) {
10050
+ validateHookInput(input);
10051
+ const hooksPath = input.scope === "global" ? path8.join(codexHome, "hooks.json") : path8.join(workspacePath, ".codex", "hooks.json");
10052
+ const config = await readJsonFileOrDefault(hooksPath);
10053
+ const eventKey = HOOK_EVENT_JSON_KEYS[input.eventName];
10054
+ const matcher = input.matcher?.trim() || null;
10055
+ const handler = {
10056
+ type: "command",
10057
+ command: input.command.trim()
10058
+ };
10059
+ if (input.timeoutSec !== void 0 && input.timeoutSec !== null) {
10060
+ handler.timeout = input.timeoutSec;
10061
+ }
10062
+ if (input.statusMessage?.trim()) {
10063
+ handler.statusMessage = input.statusMessage.trim();
10064
+ }
10065
+ const group = {
10066
+ hooks: [handler]
10067
+ };
10068
+ if (matcher) {
10069
+ group.matcher = matcher;
10070
+ }
10071
+ const currentGroups = Array.isArray(config.hooks[eventKey]) ? config.hooks[eventKey] : [];
10072
+ config.hooks[eventKey] = [...currentGroups, group];
10073
+ await fs8.mkdir(path8.dirname(hooksPath), { recursive: true });
10074
+ await fs8.writeFile(hooksPath, `${JSON.stringify(config, null, 2)}
10075
+ `, "utf8");
10076
+ }
10077
+ function hookInputMatches(group, handler, input) {
10078
+ if (!isRecord(group) || !isRecord(handler)) {
10079
+ return false;
10080
+ }
10081
+ const matcher = typeof group.matcher === "string" ? group.matcher : null;
10082
+ const handlerCommand = typeof handler.command === "string" ? handler.command : "";
10083
+ const handlerTimeout = typeof handler.timeout === "number" && Number.isFinite(handler.timeout) ? handler.timeout : null;
10084
+ const handlerStatusMessage = typeof handler.statusMessage === "string" ? handler.statusMessage : null;
10085
+ return handler.type === "command" && (input.matcher?.trim() || null) === matcher && input.command.trim() === handlerCommand && (input.timeoutSec ?? null) === handlerTimeout && (input.statusMessage?.trim() || null) === handlerStatusMessage;
10086
+ }
10087
+ async function updateHookJsonEntry({
10088
+ codexHome,
10089
+ workspacePath,
10090
+ input
10091
+ }) {
10092
+ validateHookInput(input);
10093
+ validateHookInput(input.target);
10094
+ if (input.scope !== input.target.scope) {
10095
+ throw new HttpError(400, {
10096
+ code: "bad_request",
10097
+ message: "Hook scope cannot be changed while editing."
10098
+ });
10099
+ }
10100
+ const hooksPath = input.scope === "global" ? path8.join(codexHome, "hooks.json") : path8.join(workspacePath, ".codex", "hooks.json");
10101
+ const config = await readJsonFileOrDefault(hooksPath);
10102
+ const targetEventKey = HOOK_EVENT_JSON_KEYS[input.target.eventName];
10103
+ const nextEventKey = HOOK_EVENT_JSON_KEYS[input.eventName];
10104
+ const currentGroups = Array.isArray(config.hooks[targetEventKey]) ? config.hooks[targetEventKey] : [];
10105
+ let replacementGroup = null;
10106
+ config.hooks[targetEventKey] = currentGroups.map((group) => {
10107
+ if (replacementGroup || !isRecord(group) || !Array.isArray(group.hooks)) {
10108
+ return group;
10109
+ }
10110
+ const hookIndex = group.hooks.findIndex(
10111
+ (handler2) => hookInputMatches(group, handler2, input.target)
10112
+ );
10113
+ if (hookIndex < 0) {
10114
+ return group;
10115
+ }
10116
+ const handler = {
10117
+ type: "command",
10118
+ command: input.command.trim()
10119
+ };
10120
+ if (input.timeoutSec !== void 0 && input.timeoutSec !== null) {
10121
+ handler.timeout = input.timeoutSec;
10122
+ }
10123
+ if (input.statusMessage?.trim()) {
10124
+ handler.statusMessage = input.statusMessage.trim();
10125
+ }
10126
+ replacementGroup = {
10127
+ hooks: [handler]
10128
+ };
10129
+ const matcher = input.matcher?.trim() || null;
10130
+ if (matcher) {
10131
+ replacementGroup.matcher = matcher;
10132
+ }
10133
+ if (targetEventKey !== nextEventKey) {
10134
+ const remainingHooks = group.hooks.filter((_, index) => index !== hookIndex);
10135
+ return {
10136
+ ...group,
10137
+ hooks: remainingHooks
10138
+ };
10139
+ }
10140
+ return {
10141
+ ...replacementGroup,
10142
+ hooks: group.hooks.map(
10143
+ (existing, index) => index === hookIndex ? replacementGroup.hooks[0] : existing
10144
+ )
10145
+ };
10146
+ }).filter((group) => {
10147
+ if (!isRecord(group) || !Array.isArray(group.hooks)) {
10148
+ return true;
10149
+ }
10150
+ return group.hooks.length > 0;
10151
+ });
10152
+ if (!replacementGroup) {
10153
+ throw new HttpError(404, {
10154
+ code: "not_found",
10155
+ message: "Hook was not found in hooks.json."
10156
+ });
10157
+ }
10158
+ if (targetEventKey !== nextEventKey) {
10159
+ if (config.hooks[targetEventKey]?.length === 0) {
10160
+ delete config.hooks[targetEventKey];
10161
+ }
10162
+ const nextGroups = Array.isArray(config.hooks[nextEventKey]) ? config.hooks[nextEventKey] : [];
10163
+ config.hooks[nextEventKey] = [...nextGroups, replacementGroup];
10164
+ }
10165
+ await fs8.mkdir(path8.dirname(hooksPath), { recursive: true });
10166
+ await fs8.writeFile(hooksPath, `${JSON.stringify(config, null, 2)}
10167
+ `, "utf8");
10168
+ }
10169
+ async function readLocalHookDtos({
10170
+ hooksPath,
10171
+ source,
10172
+ displayOffset
10173
+ }) {
10174
+ const config = await readJsonFileOrDefault(hooksPath);
10175
+ const hooks = [];
10176
+ for (const [eventKey, groups] of Object.entries(config.hooks)) {
10177
+ const eventName = HOOK_EVENT_DTO_KEYS[eventKey];
10178
+ if (!eventName || !Array.isArray(groups)) {
10179
+ continue;
10180
+ }
10181
+ groups.forEach((group, groupIndex) => {
10182
+ if (!isRecord(group) || !Array.isArray(group.hooks)) {
10183
+ return;
10184
+ }
10185
+ const matcher = typeof group.matcher === "string" ? group.matcher : null;
10186
+ group.hooks.forEach((handler, handlerIndex) => {
10187
+ if (!isRecord(handler) || handler.type !== "command") {
10188
+ return;
10189
+ }
10190
+ const command = typeof handler.command === "string" ? handler.command : null;
10191
+ if (!command) {
10192
+ return;
10193
+ }
10194
+ const timeoutSec = typeof handler.timeout === "number" && Number.isFinite(handler.timeout) ? handler.timeout : 600;
10195
+ const statusMessage = typeof handler.statusMessage === "string" ? handler.statusMessage : null;
10196
+ const key = `${source}:${hooksPath}:${eventKey}:${groupIndex}:${handlerIndex}`;
10197
+ hooks.push({
10198
+ key,
10199
+ eventName,
10200
+ handlerType: "command",
10201
+ matcher,
10202
+ command,
10203
+ timeoutSec,
10204
+ statusMessage,
10205
+ sourcePath: hooksPath,
10206
+ source,
10207
+ pluginId: null,
10208
+ displayOrder: displayOffset + hooks.length,
10209
+ enabled: true,
10210
+ isManaged: false,
10211
+ currentHash: "",
10212
+ trustStatus: "untrusted"
10213
+ });
10214
+ });
10215
+ });
10216
+ }
10217
+ return hooks;
10218
+ }
10219
+ function hookMatchesInput(hook, input) {
10220
+ return hook.source === input.scope && hook.eventName === input.eventName && (hook.matcher ?? null) === (input.matcher ?? null) && hook.command === input.command && (input.timeoutSec == null || hook.timeoutSec === input.timeoutSec) && (hook.statusMessage ?? null) === (input.statusMessage ?? null);
10221
+ }
10222
+ async function findOfficialHookForInput(codexManager, workspacePath, input) {
10223
+ const [entry] = await codexManager.listHooks({
10224
+ cwds: [workspacePath]
10225
+ });
10226
+ const officialHooks = (entry?.hooks ?? []).map((hook) => ({
10227
+ key: hook.key,
10228
+ eventName: hook.eventName,
10229
+ handlerType: hook.handlerType,
10230
+ matcher: hook.matcher,
10231
+ command: hook.command,
10232
+ timeoutSec: hook.timeoutSec,
10233
+ statusMessage: hook.statusMessage,
10234
+ sourcePath: hook.sourcePath,
10235
+ source: hook.source,
10236
+ pluginId: hook.pluginId,
10237
+ displayOrder: hook.displayOrder,
10238
+ enabled: hook.enabled,
10239
+ isManaged: hook.isManaged,
10240
+ currentHash: hook.currentHash,
10241
+ trustStatus: hook.trustStatus
10242
+ }));
10243
+ return officialHooks.find((hook) => hookMatchesInput(hook, input)) ?? null;
10244
+ }
10245
+ async function trustHookForInput(codexManager, workspacePath, input) {
10246
+ const hook = await findOfficialHookForInput(codexManager, workspacePath, input);
10247
+ if (!hook || !hook.key || !hook.currentHash || hook.isManaged) {
10248
+ return;
10249
+ }
10250
+ await codexManager.setHookTrust({
10251
+ key: hook.key,
10252
+ trustedHash: hook.currentHash
10253
+ });
10254
+ }
9560
10255
  function normalizeOptionLabelForApproval(value) {
9561
10256
  return value.replace(/\s*\(recommended\)\s*$/i, "").trim().toLowerCase();
9562
10257
  }
@@ -9566,7 +10261,7 @@ function isAllowOptionLabel(value) {
9566
10261
  }
9567
10262
  function isLikelyPositiveApprovalOption(value) {
9568
10263
  const normalized = normalizeOptionLabelForApproval(value);
9569
- return isAllowOptionLabel(value) || /^(yes|continue|proceed|trust|always|once|ok)\b/.test(normalized);
10264
+ return isAllowOptionLabel(normalized) || /\b(allow|approve|yes|continue|proceed|trust)\b/.test(normalized);
9570
10265
  }
9571
10266
  function isLikelyApprovalPrompt(requestMethod, questions) {
9572
10267
  const methodText = requestMethod.toLowerCase();
@@ -9782,6 +10477,31 @@ function turnToDto(turn, deferredDetails) {
9782
10477
  items: turn.items.map((item) => itemToHistoryItem(item, deferredDetails))
9783
10478
  };
9784
10479
  }
10480
+ function applyRecordedTurnItemOrder(turn, turnItemOrder) {
10481
+ const itemOrder = turnItemOrder.get(turn.id);
10482
+ if (!itemOrder || itemOrder.size === 0) {
10483
+ return turn;
10484
+ }
10485
+ let changed = false;
10486
+ const items = turn.items.map((item) => {
10487
+ const sequence = itemOrder.get(item.id);
10488
+ if (sequence === void 0 || item.sequence === sequence) {
10489
+ return item;
10490
+ }
10491
+ changed = true;
10492
+ return {
10493
+ ...item,
10494
+ sequence
10495
+ };
10496
+ });
10497
+ return changed ? { ...turn, items: sortTurnItemsByRecordedSequence(items) } : turn;
10498
+ }
10499
+ function applyRecordedTurnItemOrders(turns, turnItemOrder) {
10500
+ if (turnItemOrder.size === 0) {
10501
+ return turns;
10502
+ }
10503
+ return turns.map((turn) => applyRecordedTurnItemOrder(turn, turnItemOrder));
10504
+ }
9785
10505
  function buildTurnDto(turn, metadata) {
9786
10506
  const tokenUsage = parseThreadTurnTokenUsageJson(metadata?.tokenUsageJson);
9787
10507
  return {
@@ -9867,6 +10587,8 @@ var ThreadService = class {
9867
10587
  threadCumulativeTokenUsage = /* @__PURE__ */ new Map();
9868
10588
  threadLivePlans = /* @__PURE__ */ new Map();
9869
10589
  threadLiveItems = /* @__PURE__ */ new Map();
10590
+ threadTurnItemOrder = /* @__PURE__ */ new Map();
10591
+ threadNextTurnItemSequence = /* @__PURE__ */ new Map();
9870
10592
  answeredRequestNotes = /* @__PURE__ */ new Map();
9871
10593
  getLatestStoredThreadCumulativeTotal(localThreadId, options = {}) {
9872
10594
  const metadata = listThreadTurnMetadataByThreadId(this.db, localThreadId).filter((entry) => entry.turnId !== options.excludeTurnId).sort((left, right) => {
@@ -9923,6 +10645,30 @@ var ThreadService = class {
9923
10645
  emitEvent
9924
10646
  );
9925
10647
  }
10648
+ listPersistedHistoryItemsByTurnId(localThreadId) {
10649
+ const itemsByTurnId = /* @__PURE__ */ new Map();
10650
+ for (const record of listThreadHistoryItemRecordsByThreadId(this.db, localThreadId)) {
10651
+ const item = parseStoredHistoryItem(record.itemJson);
10652
+ if (!item) {
10653
+ continue;
10654
+ }
10655
+ const current = itemsByTurnId.get(record.turnId) ?? [];
10656
+ current.push(item);
10657
+ itemsByTurnId.set(record.turnId, current);
10658
+ }
10659
+ return itemsByTurnId;
10660
+ }
10661
+ persistLiveHistoryItem(localThreadId, turnId, item) {
10662
+ if (!shouldPersistLiveHistoryItem(item)) {
10663
+ return;
10664
+ }
10665
+ upsertThreadHistoryItemRecord(this.db, {
10666
+ threadId: localThreadId,
10667
+ turnId,
10668
+ itemId: item.id,
10669
+ itemJson: JSON.stringify(item)
10670
+ });
10671
+ }
9926
10672
  async buildThreadDetailCacheEntry(localThreadId, record, turnMetadataById) {
9927
10673
  const cached = this.getThreadDetailCache(localThreadId);
9928
10674
  if (cached) {
@@ -9939,7 +10685,15 @@ var ThreadService = class {
9939
10685
  if (!remoteThread) {
9940
10686
  const localSession = await this.localSessionStore.findSession(record.codexThreadId);
9941
10687
  const deferredDetails2 = /* @__PURE__ */ new Map();
9942
- const turns2 = (localSession?.turns ?? []).map(
10688
+ const persistedItemsByTurnId2 = this.listPersistedHistoryItemsByTurnId(localThreadId);
10689
+ const turns2 = mergePersistedHistoryItemsIntoTurns(
10690
+ applyRecordedTurnItemOrders(
10691
+ localSession?.turns ?? [],
10692
+ this.turnItemOrderSnapshot(localThreadId)
10693
+ ),
10694
+ persistedItemsByTurnId2,
10695
+ deferredDetails2
10696
+ ).map(
9943
10697
  (turn) => buildTurnDto(
9944
10698
  deferLargeHistoryItemDetails(turn, deferredDetails2),
9945
10699
  turnMetadataById.get(turn.id)
@@ -9971,8 +10725,16 @@ var ThreadService = class {
9971
10725
  );
9972
10726
  this.reconcilePendingSteers(updated.id, remoteThread);
9973
10727
  const deferredDetails = /* @__PURE__ */ new Map();
9974
- const turns = remoteThread.turns.map(
9975
- (turn) => buildTurnDto(turnToDto(turn, deferredDetails), turnMetadataById.get(turn.id))
10728
+ const persistedItemsByTurnId = this.listPersistedHistoryItemsByTurnId(localThreadId);
10729
+ const turns = mergePersistedHistoryItemsIntoTurns(
10730
+ applyRecordedTurnItemOrders(
10731
+ remoteThread.turns.map((turn) => turnToDto(turn, deferredDetails)),
10732
+ this.turnItemOrderSnapshot(localThreadId)
10733
+ ),
10734
+ persistedItemsByTurnId,
10735
+ deferredDetails
10736
+ ).map(
10737
+ (turn) => buildTurnDto(turn, turnMetadataById.get(turn.id))
9976
10738
  );
9977
10739
  const entry = {
9978
10740
  turns,
@@ -11080,6 +11842,125 @@ var ThreadService = class {
11080
11842
  }))
11081
11843
  };
11082
11844
  }
11845
+ async listThreadHooks(localThreadId) {
11846
+ const record = getThreadRecordById(this.db, localThreadId);
11847
+ if (!record) {
11848
+ throw new HttpError(404, {
11849
+ code: "not_found",
11850
+ message: "Thread was not found."
11851
+ });
11852
+ }
11853
+ const workspace = getWorkspaceRecordById(this.db, record.workspaceId);
11854
+ if (!workspace) {
11855
+ throw new HttpError(404, {
11856
+ code: "not_found",
11857
+ message: "Workspace was not found for this thread."
11858
+ });
11859
+ }
11860
+ let entry;
11861
+ let fallbackWarnings = [];
11862
+ try {
11863
+ [entry] = await this.codexManager.listHooks({
11864
+ cwds: [workspace.absPath]
11865
+ });
11866
+ } catch (error) {
11867
+ if (!isUnsupportedHooksListError(error)) {
11868
+ throw error;
11869
+ }
11870
+ fallbackWarnings = [
11871
+ "Codex app-server does not expose hooks/list yet; showing hooks parsed from hooks.json only."
11872
+ ];
11873
+ }
11874
+ return this.toThreadHooksDto(workspace.absPath, entry, fallbackWarnings);
11875
+ }
11876
+ async createThreadHook(localThreadId, input) {
11877
+ const record = getThreadRecordById(this.db, localThreadId);
11878
+ if (!record) {
11879
+ throw new HttpError(404, {
11880
+ code: "not_found",
11881
+ message: "Thread was not found."
11882
+ });
11883
+ }
11884
+ const workspace = getWorkspaceRecordById(this.db, record.workspaceId);
11885
+ if (!workspace) {
11886
+ throw new HttpError(404, {
11887
+ code: "not_found",
11888
+ message: "Workspace was not found for this thread."
11889
+ });
11890
+ }
11891
+ await writeHookJsonEntry({
11892
+ codexHome: this.codexHome,
11893
+ workspacePath: workspace.absPath,
11894
+ input
11895
+ });
11896
+ await trustHookForInput(this.codexManager, workspace.absPath, input);
11897
+ return this.listThreadHooks(localThreadId);
11898
+ }
11899
+ async updateThreadHook(localThreadId, input) {
11900
+ const record = getThreadRecordById(this.db, localThreadId);
11901
+ if (!record) {
11902
+ throw new HttpError(404, {
11903
+ code: "not_found",
11904
+ message: "Thread was not found."
11905
+ });
11906
+ }
11907
+ const workspace = getWorkspaceRecordById(this.db, record.workspaceId);
11908
+ if (!workspace) {
11909
+ throw new HttpError(404, {
11910
+ code: "not_found",
11911
+ message: "Workspace was not found for this thread."
11912
+ });
11913
+ }
11914
+ await updateHookJsonEntry({
11915
+ codexHome: this.codexHome,
11916
+ workspacePath: workspace.absPath,
11917
+ input
11918
+ });
11919
+ await trustHookForInput(this.codexManager, workspace.absPath, input);
11920
+ return this.listThreadHooks(localThreadId);
11921
+ }
11922
+ async trustThreadHook(localThreadId, input) {
11923
+ const record = getThreadRecordById(this.db, localThreadId);
11924
+ if (!record) {
11925
+ throw new HttpError(404, {
11926
+ code: "not_found",
11927
+ message: "Thread was not found."
11928
+ });
11929
+ }
11930
+ const workspace = getWorkspaceRecordById(this.db, record.workspaceId);
11931
+ if (!workspace) {
11932
+ throw new HttpError(404, {
11933
+ code: "not_found",
11934
+ message: "Workspace was not found for this thread."
11935
+ });
11936
+ }
11937
+ await this.codexManager.setHookTrust({
11938
+ key: input.key,
11939
+ trustedHash: input.currentHash
11940
+ });
11941
+ return this.listThreadHooks(localThreadId);
11942
+ }
11943
+ async untrustThreadHook(localThreadId, input) {
11944
+ const record = getThreadRecordById(this.db, localThreadId);
11945
+ if (!record) {
11946
+ throw new HttpError(404, {
11947
+ code: "not_found",
11948
+ message: "Thread was not found."
11949
+ });
11950
+ }
11951
+ const workspace = getWorkspaceRecordById(this.db, record.workspaceId);
11952
+ if (!workspace) {
11953
+ throw new HttpError(404, {
11954
+ code: "not_found",
11955
+ message: "Workspace was not found for this thread."
11956
+ });
11957
+ }
11958
+ await this.codexManager.setHookTrust({
11959
+ key: input.key,
11960
+ trustedHash: null
11961
+ });
11962
+ return this.listThreadHooks(localThreadId);
11963
+ }
11083
11964
  async interruptThread(localThreadId, requestedTurnId) {
11084
11965
  const record = getThreadRecordById(this.db, localThreadId);
11085
11966
  if (!record || !record.codexThreadId) {
@@ -11129,6 +12010,7 @@ var ThreadService = class {
11129
12010
  this.threadContextUsage.delete(localThreadId);
11130
12011
  this.threadLivePlans.delete(localThreadId);
11131
12012
  this.threadLiveItems.delete(localThreadId);
12013
+ this.clearRecordedTurnItemOrders(localThreadId);
11132
12014
  this.answeredRequestNotes.delete(localThreadId);
11133
12015
  deleteViewerSessionsByThreadId(this.db, localThreadId);
11134
12016
  deleteNotificationsByThreadId(this.db, localThreadId);
@@ -11136,6 +12018,7 @@ var ThreadService = class {
11136
12018
  deleteThreadForkRecordsByForkedThreadId(this.db, localThreadId);
11137
12019
  deleteThreadActivityNotesByThreadId(this.db, localThreadId);
11138
12020
  deleteThreadGoalRecordsByThreadId(this.db, localThreadId);
12021
+ deleteThreadHistoryItemRecordsByThreadId(this.db, localThreadId);
11139
12022
  deleteThreadPendingSteerRecordsByThreadId(this.db, localThreadId);
11140
12023
  deleteThreadTurnMetadataByThreadId(this.db, localThreadId);
11141
12024
  deleteThreadRecord(this.db, localThreadId);
@@ -11338,6 +12221,10 @@ var ThreadService = class {
11338
12221
  lastError: null,
11339
12222
  lastTurnStartedAt: (/* @__PURE__ */ new Date()).toISOString()
11340
12223
  });
12224
+ this.resetRecordedTurnItemOrder(record.id, params.turn.id);
12225
+ for (const item of params.turn.items) {
12226
+ this.recordTurnItemOrder(record.id, params.turn.id, item.id);
12227
+ }
11341
12228
  this.setLivePlan(record.id, null);
11342
12229
  this.setLiveItems(record.id, null);
11343
12230
  this.resetThreadContextUsage(record.id, true);
@@ -11365,6 +12252,33 @@ var ThreadService = class {
11365
12252
  });
11366
12253
  return;
11367
12254
  }
12255
+ case "hook/started":
12256
+ case "hook/completed": {
12257
+ const params = event.params;
12258
+ const record = getThreadRecordByCodexThreadId(this.db, params.threadId);
12259
+ if (!record) {
12260
+ return;
12261
+ }
12262
+ const turnId = params.turnId ?? record.codexTurnId;
12263
+ if (!turnId) {
12264
+ return;
12265
+ }
12266
+ const liveItem = {
12267
+ ...hookRunToHistoryItem(params.run),
12268
+ sequence: this.recordTurnItemOrder(record.id, turnId, `hook:${params.run.id}`)
12269
+ };
12270
+ this.persistLiveHistoryItem(record.id, turnId, liveItem);
12271
+ this.upsertLiveItem(record.id, turnId, liveItem);
12272
+ this.emitThreadEvent(
12273
+ event.method === "hook/started" ? "thread.item.started" : "thread.item.completed",
12274
+ record.id,
12275
+ {
12276
+ turnId,
12277
+ item: liveItem
12278
+ }
12279
+ );
12280
+ return;
12281
+ }
11368
12282
  case "item/started":
11369
12283
  case "item/completed": {
11370
12284
  const params = event.params;
@@ -11372,6 +12286,11 @@ var ThreadService = class {
11372
12286
  if (!record || !params.item) {
11373
12287
  return;
11374
12288
  }
12289
+ const sequence = this.recordTurnItemOrder(
12290
+ record.id,
12291
+ params.turnId,
12292
+ params.item.id
12293
+ );
11375
12294
  const liveItem = liveCodexItemToHistoryItem(
11376
12295
  params.item,
11377
12296
  event.method === "item/started" ? "started" : "completed"
@@ -11379,13 +12298,18 @@ var ThreadService = class {
11379
12298
  if (!liveItem) {
11380
12299
  return;
11381
12300
  }
11382
- this.upsertLiveItem(record.id, params.turnId, liveItem);
12301
+ const orderedLiveItem = {
12302
+ ...liveItem,
12303
+ sequence
12304
+ };
12305
+ this.persistLiveHistoryItem(record.id, params.turnId, orderedLiveItem);
12306
+ this.upsertLiveItem(record.id, params.turnId, orderedLiveItem);
11383
12307
  this.emitThreadEvent(
11384
12308
  event.method === "item/started" ? "thread.item.started" : "thread.item.completed",
11385
12309
  record.id,
11386
12310
  {
11387
12311
  turnId: params.turnId,
11388
- item: liveItem
12312
+ item: orderedLiveItem
11389
12313
  }
11390
12314
  );
11391
12315
  return;
@@ -11415,6 +12339,7 @@ var ThreadService = class {
11415
12339
  if (!record) {
11416
12340
  return;
11417
12341
  }
12342
+ this.recordTurnItemOrder(record.id, params.turnId, params.itemId);
11418
12343
  this.emitThreadEvent("thread.output.delta", record.id, {
11419
12344
  turnId: params.turnId,
11420
12345
  itemId: params.itemId,
@@ -11667,6 +12592,42 @@ var ThreadService = class {
11667
12592
  }
11668
12593
  this.threadLiveItems.delete(localThreadId);
11669
12594
  }
12595
+ resetRecordedTurnItemOrder(localThreadId, turnId) {
12596
+ this.threadTurnItemOrder.get(localThreadId)?.delete(turnId);
12597
+ this.threadNextTurnItemSequence.get(localThreadId)?.delete(turnId);
12598
+ }
12599
+ clearRecordedTurnItemOrders(localThreadId) {
12600
+ this.threadTurnItemOrder.delete(localThreadId);
12601
+ this.threadNextTurnItemSequence.delete(localThreadId);
12602
+ }
12603
+ recordTurnItemOrder(localThreadId, turnId, itemId) {
12604
+ let threadOrders = this.threadTurnItemOrder.get(localThreadId);
12605
+ if (!threadOrders) {
12606
+ threadOrders = /* @__PURE__ */ new Map();
12607
+ this.threadTurnItemOrder.set(localThreadId, threadOrders);
12608
+ }
12609
+ let turnOrder = threadOrders.get(turnId);
12610
+ if (!turnOrder) {
12611
+ turnOrder = /* @__PURE__ */ new Map();
12612
+ threadOrders.set(turnId, turnOrder);
12613
+ }
12614
+ const existing = turnOrder.get(itemId);
12615
+ if (existing !== void 0) {
12616
+ return existing;
12617
+ }
12618
+ let threadSequences = this.threadNextTurnItemSequence.get(localThreadId);
12619
+ if (!threadSequences) {
12620
+ threadSequences = /* @__PURE__ */ new Map();
12621
+ this.threadNextTurnItemSequence.set(localThreadId, threadSequences);
12622
+ }
12623
+ const sequence = threadSequences.get(turnId) ?? 0;
12624
+ threadSequences.set(turnId, sequence + 1);
12625
+ turnOrder.set(itemId, sequence);
12626
+ return sequence;
12627
+ }
12628
+ turnItemOrderSnapshot(localThreadId) {
12629
+ return this.threadTurnItemOrder.get(localThreadId) ?? /* @__PURE__ */ new Map();
12630
+ }
11670
12631
  getLiveItems(localThreadId, allTurns, visibleTurns = allTurns) {
11671
12632
  const current = this.threadLiveItems.get(localThreadId);
11672
12633
  if (!current) {
@@ -11688,10 +12649,65 @@ var ThreadService = class {
11688
12649
  ];
11689
12650
  this.setLiveItems(localThreadId, {
11690
12651
  turnId,
11691
- items: nextItems,
12652
+ items: sortHistoryItemsBySequence(nextItems),
11692
12653
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
11693
12654
  });
11694
12655
  }
12656
+ async toThreadHooksDto(workspacePath, entry, fallbackWarnings = []) {
12657
+ const globalHooksPath = path8.join(this.codexHome, "hooks.json");
12658
+ const projectHooksPath = path8.join(workspacePath, ".codex", "hooks.json");
12659
+ const officialHooks = (entry?.hooks ?? []).map((hook) => ({
12660
+ key: hook.key,
12661
+ eventName: hook.eventName,
12662
+ handlerType: hook.handlerType,
12663
+ matcher: hook.matcher,
12664
+ command: hook.command,
12665
+ timeoutSec: hook.timeoutSec,
12666
+ statusMessage: hook.statusMessage,
12667
+ sourcePath: hook.sourcePath,
12668
+ source: hook.source,
12669
+ pluginId: hook.pluginId,
12670
+ displayOrder: hook.displayOrder,
12671
+ enabled: hook.enabled,
12672
+ isManaged: hook.isManaged,
12673
+ currentHash: hook.currentHash,
12674
+ trustStatus: hook.trustStatus
12675
+ }));
12676
+ const [globalHooks, projectHooks] = await Promise.all([
12677
+ readLocalHookDtos({
12678
+ hooksPath: globalHooksPath,
12679
+ source: "user",
12680
+ displayOffset: officialHooks.length
12681
+ }),
12682
+ readLocalHookDtos({
12683
+ hooksPath: projectHooksPath,
12684
+ source: "project",
12685
+ displayOffset: officialHooks.length + 1e4
12686
+ })
12687
+ ]);
12688
+ const hooksBySignature = /* @__PURE__ */ new Map();
12689
+ for (const hook of [...globalHooks, ...projectHooks, ...officialHooks]) {
12690
+ const signature = [
12691
+ hook.sourcePath,
12692
+ hook.eventName,
12693
+ hook.matcher ?? "",
12694
+ hook.command ?? "",
12695
+ hook.timeoutSec,
12696
+ hook.statusMessage ?? ""
12697
+ ].join("\0");
12698
+ hooksBySignature.set(signature, hook);
12699
+ }
12700
+ return {
12701
+ cwd: entry?.cwd ?? workspacePath,
12702
+ hooks: [...hooksBySignature.values()].sort(
12703
+ (left, right) => left.displayOrder - right.displayOrder
12704
+ ),
12705
+ warnings: [...fallbackWarnings, ...entry?.warnings ?? []],
12706
+ errors: entry?.errors ?? [],
12707
+ globalHooksPath,
12708
+ projectHooksPath
12709
+ };
12710
+ }
11695
12711
  reconcileLiveItems(localThreadId, turns) {
11696
12712
  const current = this.threadLiveItems.get(localThreadId);
11697
12713
  if (!current) {
@@ -12378,6 +13394,34 @@ var forkThreadSchema = z4.discriminatedUnion("mode", [
12378
13394
  turnId: z4.string().min(1)
12379
13395
  })
12380
13396
  ]);
13397
+ var hookEventNameSchema = z4.enum([
13398
+ "preToolUse",
13399
+ "permissionRequest",
13400
+ "postToolUse",
13401
+ "preCompact",
13402
+ "postCompact",
13403
+ "sessionStart",
13404
+ "userPromptSubmit",
13405
+ "stop"
13406
+ ]);
13407
+ var createThreadHookSchema = z4.object({
13408
+ scope: z4.enum(["global", "project"]),
13409
+ eventName: hookEventNameSchema,
13410
+ matcher: z4.string().nullable().optional(),
13411
+ command: z4.string().trim().min(1),
13412
+ timeoutSec: z4.number().int().positive().max(86400).nullable().optional(),
13413
+ statusMessage: z4.string().nullable().optional()
13414
+ });
13415
+ var updateThreadHookSchema = createThreadHookSchema.extend({
13416
+ target: createThreadHookSchema
13417
+ });
13418
+ var trustThreadHookSchema = z4.object({
13419
+ key: z4.string().min(1),
13420
+ currentHash: z4.string().min(1)
13421
+ });
13422
+ var untrustThreadHookSchema = z4.object({
13423
+ key: z4.string().min(1)
13424
+ });
12381
13425
  var respondThreadRequestSchema = z4.object({
12382
13426
  answers: z4.record(z4.string(), z4.object({
12383
13427
  answers: z4.array(z4.string())
@@ -12749,6 +13793,54 @@ async function registerThreadRoutes(app2) {
12749
13793
  const params = z4.object({ id: z4.string().uuid() }).parse(request.params);
12750
13794
  return app2.services.threadService.listThreadMcpServers(params.id);
12751
13795
  });
13796
+ app2.get("/api/threads/:id/hooks", async (request) => {
13797
+ const params = z4.object({ id: z4.string().uuid() }).parse(request.params);
13798
+ return app2.services.threadService.listThreadHooks(params.id);
13799
+ });
13800
+ app2.post("/api/threads/:id/hooks", async (request) => {
13801
+ const params = z4.object({ id: z4.string().uuid() }).parse(request.params);
13802
+ const parsedBody = createThreadHookSchema.parse(request.body);
13803
+ const body = {
13804
+ scope: parsedBody.scope,
13805
+ eventName: parsedBody.eventName,
13806
+ command: parsedBody.command,
13807
+ ...parsedBody.matcher !== void 0 ? { matcher: parsedBody.matcher } : {},
13808
+ ...parsedBody.timeoutSec !== void 0 ? { timeoutSec: parsedBody.timeoutSec } : {},
13809
+ ...parsedBody.statusMessage !== void 0 ? { statusMessage: parsedBody.statusMessage } : {}
13810
+ };
13811
+ return app2.services.threadService.createThreadHook(params.id, body);
13812
+ });
13813
+ app2.put("/api/threads/:id/hooks", async (request) => {
13814
+ const params = z4.object({ id: z4.string().uuid() }).parse(request.params);
13815
+ const parsedBody = updateThreadHookSchema.parse(request.body);
13816
+ const body = {
13817
+ scope: parsedBody.scope,
13818
+ eventName: parsedBody.eventName,
13819
+ command: parsedBody.command,
13820
+ target: {
13821
+ scope: parsedBody.target.scope,
13822
+ eventName: parsedBody.target.eventName,
13823
+ command: parsedBody.target.command,
13824
+ ...parsedBody.target.matcher !== void 0 ? { matcher: parsedBody.target.matcher } : {},
13825
+ ...parsedBody.target.timeoutSec !== void 0 ? { timeoutSec: parsedBody.target.timeoutSec } : {},
13826
+ ...parsedBody.target.statusMessage !== void 0 ? { statusMessage: parsedBody.target.statusMessage } : {}
13827
+ },
13828
+ ...parsedBody.matcher !== void 0 ? { matcher: parsedBody.matcher } : {},
13829
+ ...parsedBody.timeoutSec !== void 0 ? { timeoutSec: parsedBody.timeoutSec } : {},
13830
+ ...parsedBody.statusMessage !== void 0 ? { statusMessage: parsedBody.statusMessage } : {}
13831
+ };
13832
+ return app2.services.threadService.updateThreadHook(params.id, body);
13833
+ });
13834
+ app2.post("/api/threads/:id/hooks/trust", async (request) => {
13835
+ const params = z4.object({ id: z4.string().uuid() }).parse(request.params);
13836
+ const body = trustThreadHookSchema.parse(request.body);
13837
+ return app2.services.threadService.trustThreadHook(params.id, body);
13838
+ });
13839
+ app2.post("/api/threads/:id/hooks/untrust", async (request) => {
13840
+ const params = z4.object({ id: z4.string().uuid() }).parse(request.params);
13841
+ const body = untrustThreadHookSchema.parse(request.body);
13842
+ return app2.services.threadService.untrustThreadHook(params.id, body);
13843
+ });
12752
13844
  app2.post("/api/threads/:id/resume", async (request) => {
12753
13845
  const params = z4.object({ id: z4.string().uuid() }).parse(request.params);
12754
13846
  const body = resumeThreadSchema.parse(request.body ?? {});