@cocorograph/hub-agent 0.8.70 → 0.8.72
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/package.json +1 -1
- package/src/codex-session-operations.mjs +83 -0
- package/src/codex-stream-bridge.mjs +73 -8
- package/src/main.mjs +54 -2
- package/src/usage.mjs +37 -9
- package/src/ws-client.mjs +2 -0
package/package.json
CHANGED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { createReadStream } from "node:fs"
|
|
2
|
+
import { createInterface } from "node:readline"
|
|
3
|
+
import path from "node:path"
|
|
4
|
+
import os from "node:os"
|
|
5
|
+
|
|
6
|
+
export function validateCodexSessionOperation({ operation, cwd, session_id, title, configDir }) {
|
|
7
|
+
if (typeof cwd !== "string" || !path.isAbsolute(cwd)) return "cwd required"
|
|
8
|
+
if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(session_id)) return "invalid session_id"
|
|
9
|
+
if (typeof configDir !== "string" || !path.isAbsolute(configDir)) return "unknown profile"
|
|
10
|
+
if (operation !== "rename" && operation !== "delete") return "unsupported operation"
|
|
11
|
+
if (operation === "rename") {
|
|
12
|
+
if (typeof title !== "string") return "title must be a string"
|
|
13
|
+
const name = title.trim().replace(/\s+/g, " ")
|
|
14
|
+
if (!name) return "title required"
|
|
15
|
+
if (Array.from(name).length > 200) return "title too long"
|
|
16
|
+
}
|
|
17
|
+
return null
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Use only the selected profile's app-server. Never fall back to archive or local deletion. */
|
|
21
|
+
export async function operateCodexSession(options) {
|
|
22
|
+
const error = validateCodexSessionOperation(options)
|
|
23
|
+
if (error) return { ok: false, error }
|
|
24
|
+
const { operation, cwd, session_id, title, configDir, clientFactory, beforeMutation, logger } = options
|
|
25
|
+
let client
|
|
26
|
+
try {
|
|
27
|
+
client = clientFactory({ cwd, env: { ...process.env, CODEX_HOME: configDir } })
|
|
28
|
+
client.on("error", (err) => {
|
|
29
|
+
logger?.warn?.({ operation, code: err?.code, reason: err?.reason }, "codex session client error")
|
|
30
|
+
})
|
|
31
|
+
client.start()
|
|
32
|
+
await client.request("initialize", { clientInfo: { name: "cockpit-session-operations", version: "1.0.0" } })
|
|
33
|
+
client.notify("initialized", {})
|
|
34
|
+
const result = await client.request("thread/read", { threadId: session_id, includeTurns: false })
|
|
35
|
+
if (result?.thread?.id !== session_id || result.thread.cwd !== cwd) {
|
|
36
|
+
return { ok: false, error: "session context mismatch" }
|
|
37
|
+
}
|
|
38
|
+
const blocked = beforeMutation?.()
|
|
39
|
+
if (blocked) return { ok: false, error: blocked }
|
|
40
|
+
if (operation === "rename") {
|
|
41
|
+
const custom_title = title.trim().replace(/\s+/g, " ")
|
|
42
|
+
await client.request("thread/name/set", { threadId: session_id, name: custom_title })
|
|
43
|
+
return { ok: true, custom_title }
|
|
44
|
+
}
|
|
45
|
+
await client.request("thread/delete", { threadId: session_id })
|
|
46
|
+
return { ok: true }
|
|
47
|
+
} catch (err) {
|
|
48
|
+
// RPC errors may include paths or user data. Return a fixed public error only.
|
|
49
|
+
logger?.warn?.({ operation, code: err?.code, reason: err?.reason }, "codex session operation failed")
|
|
50
|
+
return { ok: false, error: err?.code === -32601 ? "session operation unsupported" : `${operation} failed` }
|
|
51
|
+
} finally {
|
|
52
|
+
client?.stop()
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Titles are persisted by Codex in the profile's append-only session index. */
|
|
57
|
+
export async function addCodexSessionTitles(sessions, sessionsDir) {
|
|
58
|
+
const root = path.resolve(sessionsDir || process.env.HUB_CODEX_SESSIONS_DIR || path.join(process.env.CODEX_HOME || path.join(os.homedir(), ".codex"), "sessions"))
|
|
59
|
+
const wanted = new Set(sessions.map((session) => session.thread_id))
|
|
60
|
+
if (!wanted.size) return sessions
|
|
61
|
+
const titles = new Map()
|
|
62
|
+
const input = createReadStream(path.join(path.dirname(root), "session_index.jsonl"), { encoding: "utf8" })
|
|
63
|
+
const lines = createInterface({ input, crlfDelay: Infinity })
|
|
64
|
+
try {
|
|
65
|
+
for await (const line of lines) {
|
|
66
|
+
let row
|
|
67
|
+
try { row = JSON.parse(line) } catch { continue }
|
|
68
|
+
if (!wanted.has(row?.id) || typeof row.thread_name !== "string") continue
|
|
69
|
+
const time = Date.parse(row.updated_at)
|
|
70
|
+
if (!Number.isFinite(time)) continue
|
|
71
|
+
const previous = titles.get(row.id)
|
|
72
|
+
if (!previous || time >= previous.time) titles.set(row.id, { title: row.thread_name, time })
|
|
73
|
+
}
|
|
74
|
+
} catch {
|
|
75
|
+
return sessions
|
|
76
|
+
} finally {
|
|
77
|
+
lines.close()
|
|
78
|
+
input.destroy()
|
|
79
|
+
}
|
|
80
|
+
return sessions.map((session) => titles.has(session.thread_id)
|
|
81
|
+
? { ...session, title: titles.get(session.thread_id).title }
|
|
82
|
+
: session)
|
|
83
|
+
}
|
|
@@ -42,11 +42,14 @@
|
|
|
42
42
|
*/
|
|
43
43
|
import { EventEmitter } from "node:events"
|
|
44
44
|
import { randomUUID } from "node:crypto"
|
|
45
|
+
import path from "node:path"
|
|
45
46
|
|
|
46
47
|
import { recordInjectSourcePersistent } from "./inject-source-store.mjs"
|
|
47
48
|
|
|
48
49
|
import { AsyncTtlCache } from "./async-ttl-cache.mjs"
|
|
49
50
|
import { CodexAppServerClient } from "./codex-appserver-client.mjs"
|
|
51
|
+
import { addCodexSessionTitles, operateCodexSession, validateCodexSessionOperation } from "./codex-session-operations.mjs"
|
|
52
|
+
import { defaultCodexDir } from "./codex-profiles.mjs"
|
|
50
53
|
import {
|
|
51
54
|
buildCodexArtifactTemplatePickerConfig,
|
|
52
55
|
preflightCodexArtifactTemplatePicker,
|
|
@@ -949,6 +952,7 @@ export class CodexStreamBridge extends EventEmitter {
|
|
|
949
952
|
// #1 in-memory reattach のキー。attach 試行を跨いで安定 (WS 再接続で stream_id は変わる)。
|
|
950
953
|
attachId: attachId || null,
|
|
951
954
|
client,
|
|
955
|
+
codexHome: client.env?.CODEX_HOME || process.env.CODEX_HOME || defaultCodexDir(),
|
|
952
956
|
threadId: null,
|
|
953
957
|
// 新規 thread/start は、最初の usage.response を受け取るまで context 使用量が 0%。
|
|
954
958
|
// state loop が同じ cwd の古い TUI 値を拾って上書きしないための境界マーカー。
|
|
@@ -976,6 +980,8 @@ export class CodexStreamBridge extends EventEmitter {
|
|
|
976
980
|
/** 取消成功 id は accepted(二重実行ガード)から外さず、browser が「accepted +キュー不在」
|
|
977
981
|
* のバブル昇格から除外するための別軸として保持する。 */
|
|
978
982
|
cancelledInputIds: [],
|
|
983
|
+
/** 取消が input より先着した場合の guard。後着 input で消費する。 */
|
|
984
|
+
canceledInputIds: new Set(),
|
|
979
985
|
/** @type {{resolve: Function, timer: any} | null} round3 major#2: turn/start 後に turn/started
|
|
980
986
|
* (activeTurnId 確定) を待つ barrier の待機者。timeout 付きでハングを防ぐ。 */
|
|
981
987
|
_turnActiveWaiter: null,
|
|
@@ -1961,7 +1967,42 @@ export class CodexStreamBridge extends EventEmitter {
|
|
|
1961
1967
|
// Phase 3 (Read-plane スライス2a): app-server を spawn せず rollout の disk 走査で一覧を作る。
|
|
1962
1968
|
// updated_at_ms は rollout ファイルの mtime (= 最終書き込み時刻 = ターン境界に相当) で、
|
|
1963
1969
|
// claude 側 ClaudeSessionSummary.mtime と単位・意味を揃える。並びは mtime desc。
|
|
1964
|
-
|
|
1970
|
+
const sessions = await this._listThreadsFromDisk({ cwd, limit, sessionsDir, logger: this.logger })
|
|
1971
|
+
return addCodexSessionTitles(sessions, sessionsDir)
|
|
1972
|
+
}
|
|
1973
|
+
|
|
1974
|
+
async operateSession(options) {
|
|
1975
|
+
const invalid = validateCodexSessionOperation(options)
|
|
1976
|
+
if (invalid) return { ok: false, error: invalid }
|
|
1977
|
+
const { operation, cwd, session_id, configDir } = options
|
|
1978
|
+
const candidates = () => this._liveSessions().filter((session) =>
|
|
1979
|
+
session.cwd === cwd && (session.threadId || session.resumeThreadId) === session_id)
|
|
1980
|
+
const matching = () => candidates().filter((session) =>
|
|
1981
|
+
path.resolve(session.codexHome) === path.resolve(configDir))
|
|
1982
|
+
const busy = () => {
|
|
1983
|
+
const live = candidates()
|
|
1984
|
+
const selected = matching()
|
|
1985
|
+
// Shared rollout paths mean a profile mismatch is not proof that deletion is safe.
|
|
1986
|
+
if (live.length !== selected.length) return "session busy"
|
|
1987
|
+
return operation === "delete" && selected.some((session) =>
|
|
1988
|
+
session.activeTurnId || this._isProtected(session, Date.now())) ? "session busy" : null
|
|
1989
|
+
}
|
|
1990
|
+
const blocked = busy()
|
|
1991
|
+
if (blocked) return { ok: false, error: blocked }
|
|
1992
|
+
const result = await operateCodexSession({
|
|
1993
|
+
...options,
|
|
1994
|
+
clientFactory: (opts) => this._clientFactory({ ...opts, codexCmd: this.codexCmd, logger: this.logger }),
|
|
1995
|
+
logger: this.logger,
|
|
1996
|
+
beforeMutation: busy,
|
|
1997
|
+
})
|
|
1998
|
+
if (!result.ok) return result
|
|
1999
|
+
const deletedLive = operation === "delete" ? matching() : []
|
|
2000
|
+
const response = { ...result, ...(operation === "delete" ? { stopped: deletedLive.length } : {}) }
|
|
2001
|
+
this._invalidateListThreadsCacheForCwd(cwd)
|
|
2002
|
+
// The request must settle before exit clears the browser's pending requests.
|
|
2003
|
+
options.onSuccess?.(response)
|
|
2004
|
+
for (const session of deletedLive) this._reapSession(session, Date.now(), "session-deleted")
|
|
2005
|
+
return response
|
|
1965
2006
|
}
|
|
1966
2007
|
|
|
1967
2008
|
/**
|
|
@@ -2127,16 +2168,34 @@ export class CodexStreamBridge extends EventEmitter {
|
|
|
2127
2168
|
/** 次ターン FIFO に残っている browser 入力を id 指定で取り消す。
|
|
2128
2169
|
* dispatchingInput は既に turn/start RPC 中なので対象外(実行開始済みとして扱う)。 */
|
|
2129
2170
|
cancelQueued({ stream_id, id }) {
|
|
2130
|
-
const session = this.
|
|
2131
|
-
if (!
|
|
2171
|
+
const session = this.sessions.get(stream_id)
|
|
2172
|
+
if (!session || session.dead) {
|
|
2173
|
+
return { outcome: "stream_missing", flush: () => {} }
|
|
2174
|
+
}
|
|
2175
|
+
if (typeof id !== "string" || !id) throw new Error("invalid_input_id")
|
|
2132
2176
|
const index = session.inputQueue.findIndex(
|
|
2133
2177
|
(item) =>
|
|
2134
2178
|
(item?.inputId === id || item?.deliveryId === id) &&
|
|
2135
2179
|
item !== session.dispatchingInput,
|
|
2136
2180
|
)
|
|
2137
2181
|
if (index < 0) {
|
|
2138
|
-
|
|
2139
|
-
|
|
2182
|
+
// accepted は二重実行防止の記録。取消済みなら再要求でも実行開始済みに変えない。
|
|
2183
|
+
if (session.cancelledInputIds?.includes(id)) {
|
|
2184
|
+
return { outcome: "cancelled", flush: () => this._emitQueueState(session) }
|
|
2185
|
+
}
|
|
2186
|
+
const dispatching = session.dispatchingInput
|
|
2187
|
+
const alreadyStarted =
|
|
2188
|
+
dispatching?.inputId === id ||
|
|
2189
|
+
dispatching?.deliveryId === id ||
|
|
2190
|
+
session.acceptedInputIds?.includes(id)
|
|
2191
|
+
if (!alreadyStarted) {
|
|
2192
|
+
if (session.canceledInputIds.size >= 1024) session.canceledInputIds.clear()
|
|
2193
|
+
session.canceledInputIds.add(id)
|
|
2194
|
+
}
|
|
2195
|
+
return {
|
|
2196
|
+
outcome: alreadyStarted ? "already_started" : "cancelled",
|
|
2197
|
+
flush: () => this._emitQueueState(session),
|
|
2198
|
+
}
|
|
2140
2199
|
}
|
|
2141
2200
|
const [item] = session.inputQueue.splice(index, 1)
|
|
2142
2201
|
this._recordCancelledInput(session, item.inputId || item.deliveryId)
|
|
@@ -2146,8 +2205,8 @@ export class CodexStreamBridge extends EventEmitter {
|
|
|
2146
2205
|
})
|
|
2147
2206
|
item.reject(err)
|
|
2148
2207
|
}
|
|
2149
|
-
|
|
2150
|
-
return
|
|
2208
|
+
// main が outcome を送った後で snapshot を送る。
|
|
2209
|
+
return { outcome: "cancelled", flush: () => this._emitQueueState(session) }
|
|
2151
2210
|
}
|
|
2152
2211
|
|
|
2153
2212
|
_emitQueueState(session, startedItem = null) {
|
|
@@ -3534,6 +3593,12 @@ export class CodexStreamBridge extends EventEmitter {
|
|
|
3534
3593
|
this._touch(session, stream_id)
|
|
3535
3594
|
const input = userInputFromMessage(message)
|
|
3536
3595
|
const text = input.map((part) => part?.text || "").join("\n")
|
|
3596
|
+
if (inputId && session.canceledInputIds.delete(inputId)) {
|
|
3597
|
+
this._recordAcceptedInput(session, inputId)
|
|
3598
|
+
this._recordCancelledInput(session, inputId)
|
|
3599
|
+
this._emitQueueState(session)
|
|
3600
|
+
return { method: "queued", deliveryId }
|
|
3601
|
+
}
|
|
3537
3602
|
if (inputId && session.acceptedInputIds?.includes(inputId)) {
|
|
3538
3603
|
return { method: session.activeTurnId ? "queued" : "turn/start", deliveryId }
|
|
3539
3604
|
}
|
|
@@ -4572,7 +4637,7 @@ export class CodexStreamBridge extends EventEmitter {
|
|
|
4572
4637
|
producer_seq: session.seq,
|
|
4573
4638
|
thread_id: session.threadId,
|
|
4574
4639
|
code: 0,
|
|
4575
|
-
reason: "idle-reaped",
|
|
4640
|
+
reason: reason === "session-deleted" ? "session-deleted" : "idle-reaped",
|
|
4576
4641
|
})
|
|
4577
4642
|
}
|
|
4578
4643
|
|
package/src/main.mjs
CHANGED
|
@@ -2331,6 +2331,8 @@ export function buildCodexReadyPayload(streamId, info, queueState) {
|
|
|
2331
2331
|
// Browser は旧 hub-agent との混在期間、この明示 capability が true のときだけ
|
|
2332
2332
|
// 子 thread の個別停止 UI を有効化する。省略は未対応を意味する。
|
|
2333
2333
|
supports_subagent_interrupt: true,
|
|
2334
|
+
// 旧 agent との混在時、取消結果を待てることを明示する。
|
|
2335
|
+
supports_queue_cancel_response: true,
|
|
2334
2336
|
}
|
|
2335
2337
|
if (Number.isInteger(info?.historyStart)) payload.history_start = info.historyStart
|
|
2336
2338
|
if (Number.isInteger(info?.historyTotal)) payload.history_total = info.historyTotal
|
|
@@ -4797,9 +4799,16 @@ export async function dispatch(msg, ctx) {
|
|
|
4797
4799
|
// turn/steer / turn/interrupt は明示操作の別経路として扱う。
|
|
4798
4800
|
handleCodexChatInput(msg, ctx)
|
|
4799
4801
|
return
|
|
4800
|
-
case "codex.queue.cancel":
|
|
4802
|
+
case "codex.queue.cancel": {
|
|
4803
|
+
const cancelRes = {
|
|
4804
|
+
type: "codex.queue.response",
|
|
4805
|
+
stream_id: msg.stream_id,
|
|
4806
|
+
request_id: msg.request_id || null,
|
|
4807
|
+
input_id: msg.id || null,
|
|
4808
|
+
}
|
|
4809
|
+
let canceled
|
|
4801
4810
|
try {
|
|
4802
|
-
ctx.codexBridge?.cancelQueued({
|
|
4811
|
+
canceled = ctx.codexBridge?.cancelQueued({
|
|
4803
4812
|
stream_id: msg.stream_id,
|
|
4804
4813
|
id: msg.id,
|
|
4805
4814
|
})
|
|
@@ -4808,8 +4817,13 @@ export async function dispatch(msg, ctx) {
|
|
|
4808
4817
|
{ err: err?.message, stream_id: msg.stream_id, input_id: msg.id },
|
|
4809
4818
|
"codex queued input cancel failed",
|
|
4810
4819
|
)
|
|
4820
|
+
ctx.client.send({ ...cancelRes, error: err?.message || "cancel_failed" })
|
|
4821
|
+
return
|
|
4811
4822
|
}
|
|
4823
|
+
ctx.client.send({ ...cancelRes, outcome: canceled?.outcome || "stream_missing" })
|
|
4824
|
+
canceled?.flush?.()
|
|
4812
4825
|
return
|
|
4826
|
+
}
|
|
4813
4827
|
case "codex.permission.reply":
|
|
4814
4828
|
if (!ctx.codexBridge) return
|
|
4815
4829
|
ctx.codexBridge.permissionReply({
|
|
@@ -4884,6 +4898,7 @@ export async function dispatch(msg, ctx) {
|
|
|
4884
4898
|
epoch,
|
|
4885
4899
|
profile_id: profileId,
|
|
4886
4900
|
sessions: withLive,
|
|
4901
|
+
session_operations: true,
|
|
4887
4902
|
})
|
|
4888
4903
|
} catch (err) {
|
|
4889
4904
|
ctx.client.send({
|
|
@@ -4898,6 +4913,43 @@ export async function dispatch(msg, ctx) {
|
|
|
4898
4913
|
}
|
|
4899
4914
|
return
|
|
4900
4915
|
}
|
|
4916
|
+
case "codex.session.rename":
|
|
4917
|
+
case "codex.session.delete": {
|
|
4918
|
+
const operation = msg.type === "codex.session.rename" ? "rename" : "delete"
|
|
4919
|
+
const context = {
|
|
4920
|
+
request_id: typeof msg.request_id === "string" ? msg.request_id : undefined,
|
|
4921
|
+
stream_id: msg.stream_id,
|
|
4922
|
+
cwd: typeof msg.cwd === "string" ? msg.cwd : "",
|
|
4923
|
+
session_id: typeof msg.session_id === "string" ? msg.session_id : "",
|
|
4924
|
+
profile_id: typeof msg.profile_id === "string" ? msg.profile_id : undefined,
|
|
4925
|
+
}
|
|
4926
|
+
let result = { ok: false, error: "unknown profile" }
|
|
4927
|
+
let responded = false
|
|
4928
|
+
const respond = (response) => {
|
|
4929
|
+
ctx.client.send({ type: `${msg.type}.response`, ...context, ...response })
|
|
4930
|
+
responded = true
|
|
4931
|
+
}
|
|
4932
|
+
try {
|
|
4933
|
+
const profileApi = ctx.codexProfiles || codexProfiles
|
|
4934
|
+
const resolved = context.profile_id ? await profileApi.resolveConfigDir(context.profile_id) : null
|
|
4935
|
+
const active = resolved?.resolved ? await profileApi.getActiveProfile() : null
|
|
4936
|
+
if (resolved?.resolved && resolved.profileId === context.profile_id && active?.id === resolved.profileId) {
|
|
4937
|
+
result = ctx.codexBridge
|
|
4938
|
+
? await ctx.codexBridge.operateSession({
|
|
4939
|
+
operation, cwd: context.cwd, session_id: context.session_id,
|
|
4940
|
+
title: msg.title, configDir: resolved.configDir,
|
|
4941
|
+
onSuccess: respond,
|
|
4942
|
+
})
|
|
4943
|
+
: { ok: false, error: "codex unavailable" }
|
|
4944
|
+
} else if (resolved?.resolved && active?.id !== resolved.profileId) {
|
|
4945
|
+
result = { ok: false, error: "inactive profile" }
|
|
4946
|
+
}
|
|
4947
|
+
} catch {
|
|
4948
|
+
result = { ok: false, error: `${operation} failed` }
|
|
4949
|
+
}
|
|
4950
|
+
if (!responded) respond(result)
|
|
4951
|
+
return
|
|
4952
|
+
}
|
|
4901
4953
|
case "codex.history.request": {
|
|
4902
4954
|
const stream_id = msg.stream_id
|
|
4903
4955
|
const thread_id = msg.thread_id
|
package/src/usage.mjs
CHANGED
|
@@ -23,6 +23,7 @@ import path from "node:path"
|
|
|
23
23
|
import { StringDecoder } from "node:string_decoder"
|
|
24
24
|
|
|
25
25
|
import { encodeCwdToDirName } from "./claude-history.mjs"
|
|
26
|
+
import { redactSecrets } from "./tool-result-mask.mjs"
|
|
26
27
|
|
|
27
28
|
function configPath(envKey, ...fallback) {
|
|
28
29
|
return process.env[envKey] || path.join(...fallback)
|
|
@@ -1468,7 +1469,7 @@ export async function codexRolloutPresence(threadId) {
|
|
|
1468
1469
|
// 4 フィールド (thread_id / cwd / preview / updated_at_ms) はすべて rollout から安価に取れる:
|
|
1469
1470
|
// - thread_id: session_meta.session_id (無ければファイル名末尾の UUID)
|
|
1470
1471
|
// - cwd: session_meta.payload.cwd (先頭行)
|
|
1471
|
-
// - preview:
|
|
1472
|
+
// - preview: 先頭近傍の user_message または response_item の実ユーザー本文
|
|
1472
1473
|
// - updated_at_ms: ファイル mtime (Claude 側 listSessions の mtime と単位を揃える)
|
|
1473
1474
|
// ---------------------------------------------------------------------------
|
|
1474
1475
|
|
|
@@ -1482,6 +1483,23 @@ function threadIdFromRolloutFilename(fp) {
|
|
|
1482
1483
|
const CODEX_ROLLOUT_HEAD_CHUNK_BYTES = 32 * 1024
|
|
1483
1484
|
const CODEX_ROLLOUT_PREVIEW_MAX_BYTES = 1024 * 1024
|
|
1484
1485
|
|
|
1486
|
+
function codexUserPreviewText(text) {
|
|
1487
|
+
if (typeof text !== "string") return ""
|
|
1488
|
+
let remaining = text.trim()
|
|
1489
|
+
// Codex は初期コンテキストも role=user の input_text に保存する。
|
|
1490
|
+
// 既知の注入ブロックだけを先頭から除き、後続に依頼文があれば残す。
|
|
1491
|
+
while (remaining) {
|
|
1492
|
+
if (/^# AGENTS\.md instructions(?:\s|$)/.test(remaining)) return ""
|
|
1493
|
+
const injected = /^<(recommended_plugins|environment_context|user_instructions|skills_instructions)>/.exec(remaining)
|
|
1494
|
+
if (!injected) break
|
|
1495
|
+
const endTag = `</${injected[1]}>`
|
|
1496
|
+
const end = remaining.indexOf(endTag, injected[0].length)
|
|
1497
|
+
if (end < 0) return ""
|
|
1498
|
+
remaining = remaining.slice(end + endTag.length).trimStart()
|
|
1499
|
+
}
|
|
1500
|
+
return redactSecrets(remaining).replace(/\s+/g, " ").trim()
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1485
1503
|
// sessionsDir ごとの派生インデックス。rollout 自体が正本なので永続化は不要。
|
|
1486
1504
|
// _codexTokenCountMemo と同じ mtime+size 無効化規約で、変更ファイルだけを再解析する。
|
|
1487
1505
|
// 共通 helper 化は usage 集計側への回帰範囲を広げるため、一覧用 index として独立させる。
|
|
@@ -1561,14 +1579,24 @@ async function parseCodexRolloutHead(fp, expectedCwd = "", testHooks = null) {
|
|
|
1561
1579
|
subagent != null &&
|
|
1562
1580
|
typeof subagent === "object" &&
|
|
1563
1581
|
Object.prototype.hasOwnProperty.call(subagent, "thread_spawn")
|
|
1564
|
-
} else if (
|
|
1565
|
-
|
|
1566
|
-
o.type === "event_msg" &&
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1582
|
+
} else if (!preview) {
|
|
1583
|
+
let userText = ""
|
|
1584
|
+
if (o.type === "event_msg" && o.payload?.type === "user_message") {
|
|
1585
|
+
userText = codexUserPreviewText(o.payload.message)
|
|
1586
|
+
} else if (
|
|
1587
|
+
o.type === "response_item" &&
|
|
1588
|
+
o.payload?.type === "message" &&
|
|
1589
|
+
o.payload.role === "user" &&
|
|
1590
|
+
Array.isArray(o.payload.content)
|
|
1591
|
+
) {
|
|
1592
|
+
userText = o.payload.content
|
|
1593
|
+
.filter((block) => block?.type === "input_text")
|
|
1594
|
+
.map((block) => codexUserPreviewText(block.text))
|
|
1595
|
+
.filter(Boolean)
|
|
1596
|
+
.join(" ")
|
|
1597
|
+
}
|
|
1598
|
+
// code point 単位で切り、絵文字などのサロゲートペアを壊さない。
|
|
1599
|
+
preview = Array.from(userText).slice(0, 80).join("")
|
|
1572
1600
|
}
|
|
1573
1601
|
}
|
|
1574
1602
|
|
package/src/ws-client.mjs
CHANGED