remote-codex 0.1.4 → 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.
- package/apps/supervisor-api/dist/index.js +1116 -24
- package/apps/supervisor-web/dist/assets/{highlighted-body-OFNGDK62-D-RjOTTL.js → highlighted-body-OFNGDK62-DvEvXPo5.js} +1 -1
- package/apps/supervisor-web/dist/assets/index-CQu6sRq7.css +32 -0
- package/apps/supervisor-web/dist/assets/index-MELw9ga_.js +377 -0
- package/apps/supervisor-web/dist/assets/{xterm-D8iZbRww.js → xterm-CWQ1ih_R.js} +1 -1
- package/apps/supervisor-web/dist/index.html +2 -2
- package/package.json +1 -1
- package/packages/db/migrations/0014_thread_history_items.sql +12 -0
- package/scripts/service-manager.mjs +22 -4
- package/apps/supervisor-web/dist/assets/index-CdG3ogmZ.js +0 -376
- package/apps/supervisor-web/dist/assets/index-QM8NQf3e.css +0 -32
|
@@ -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(/"/g, '"').replace(/'/g, "'").replace(/>/g, ">").replace(/</g, "<").replace(/&/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
|
|
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
|
|
9551
|
-
|
|
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(
|
|
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
|
|
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
|
|
9975
|
-
|
|
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
|
-
|
|
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:
|
|
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 ?? {});
|