@cocorograph/hub-agent 0.6.19 → 0.6.21
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/claude-stream-bridge.mjs +12 -1
- package/src/main.mjs +36 -2
- package/src/service-install.mjs +73 -3
- package/src/usage.mjs +198 -0
package/package.json
CHANGED
|
@@ -389,12 +389,17 @@ class ClaudeStreamSession {
|
|
|
389
389
|
// 走行中ターンは完走を待ち TTL 後に再チェック (走行中は絶対に撤去しない)
|
|
390
390
|
if (this._busy) {
|
|
391
391
|
this._idleTimer = setTimeout(tick, ttlMs)
|
|
392
|
+
// idle TTL は最大 7 日。タイマー単独でイベントループを生かさない (unref)。
|
|
393
|
+
// 本番デーモンは WS 接続等で常駐するので reap は問題なく発火し、テスト/CLI
|
|
394
|
+
// など他に handle が無いプロセスは即座に終了できる。
|
|
395
|
+
this._idleTimer.unref?.()
|
|
392
396
|
return
|
|
393
397
|
}
|
|
394
398
|
this._idleTimer = null
|
|
395
399
|
onTimeout?.()
|
|
396
400
|
}
|
|
397
401
|
this._idleTimer = setTimeout(tick, ttlMs)
|
|
402
|
+
this._idleTimer.unref?.()
|
|
398
403
|
}
|
|
399
404
|
|
|
400
405
|
/**
|
|
@@ -897,7 +902,13 @@ export class ClaudeStreamBridge extends EventEmitter {
|
|
|
897
902
|
logger: this.logger,
|
|
898
903
|
onEvent: (event) => {
|
|
899
904
|
// stream_id は再アタッチで変わるため session.stream_id (最新) を使う。
|
|
900
|
-
|
|
905
|
+
// cwd は per-session の context% ドーナツ書き出し (usage.mjs) で使う。
|
|
906
|
+
this.emit("event", {
|
|
907
|
+
stream_id: session.stream_id,
|
|
908
|
+
session_id: session.sessionId,
|
|
909
|
+
cwd: session.cwd,
|
|
910
|
+
event,
|
|
911
|
+
})
|
|
901
912
|
// session_id 確定後は索引に登録 (冪等)。再接続時の再アタッチに使う。
|
|
902
913
|
if (session.sessionId) this._liveBySession.set(session.sessionId, session)
|
|
903
914
|
},
|
package/src/main.mjs
CHANGED
|
@@ -37,7 +37,13 @@ import {
|
|
|
37
37
|
listWorktreeStubs,
|
|
38
38
|
removeWorktree as removeWorktreeDir,
|
|
39
39
|
} from "./tmux.mjs"
|
|
40
|
-
import {
|
|
40
|
+
import {
|
|
41
|
+
getSessionUsages,
|
|
42
|
+
getUsage,
|
|
43
|
+
recordChatRateLimit,
|
|
44
|
+
recordChatSessionContext,
|
|
45
|
+
recordChatSessionStatus,
|
|
46
|
+
} from "./usage.mjs"
|
|
41
47
|
|
|
42
48
|
const logger = pino({ name: "hub-agent" })
|
|
43
49
|
|
|
@@ -178,7 +184,7 @@ export async function startDaemon({ version, ptyModule, claudeSdk } = {}) {
|
|
|
178
184
|
// そのまま browser に転送する。SDK 未インストール時は claudeBridge=null で全 attach が
|
|
179
185
|
// claude.error を返す経路に分岐 (dispatch 側で判定)。
|
|
180
186
|
if (claudeBridge) {
|
|
181
|
-
claudeBridge.on("event", ({ stream_id, session_id, event }) => {
|
|
187
|
+
claudeBridge.on("event", ({ stream_id, session_id, cwd, event }) => {
|
|
182
188
|
// SDK の rate_limit_event を捕捉して usage の 5h/7d 実値に反映する
|
|
183
189
|
// (チャットモードでは statusLine が更新されないため、これが live 値の唯一の源)。
|
|
184
190
|
if (event?.type === "rate_limit_event" && event.rate_limit_info) {
|
|
@@ -188,6 +194,34 @@ export async function startDaemon({ version, ptyModule, claudeSdk } = {}) {
|
|
|
188
194
|
/* ignore */
|
|
189
195
|
}
|
|
190
196
|
}
|
|
197
|
+
// assistant メッセージの usage から session 別 context% を算出してファイルへ
|
|
198
|
+
// 書き出す (サイドバー各行のドーナツ用。これも statusLine がチャットでは更新
|
|
199
|
+
// されないための補完)。あわせてターン状態を更新する:
|
|
200
|
+
// - assistant (生成中) → processing
|
|
201
|
+
// - result (ターン完了/入力待ち) → waiting
|
|
202
|
+
// webapp のステータスドットは tmux ペインのスクレイプで判定するが、チャットは
|
|
203
|
+
// ペインに TUI を出さないため、この chat_status を優先させて補完する。
|
|
204
|
+
const sid = session_id || event?.session_id
|
|
205
|
+
if (event?.type === "assistant") {
|
|
206
|
+
if (event.message?.usage) {
|
|
207
|
+
try {
|
|
208
|
+
recordChatSessionContext({ sessionId: sid, cwd, message: event.message })
|
|
209
|
+
} catch {
|
|
210
|
+
/* ignore */
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
try {
|
|
214
|
+
recordChatSessionStatus({ sessionId: sid, cwd, status: "processing" })
|
|
215
|
+
} catch {
|
|
216
|
+
/* ignore */
|
|
217
|
+
}
|
|
218
|
+
} else if (event?.type === "result") {
|
|
219
|
+
try {
|
|
220
|
+
recordChatSessionStatus({ sessionId: sid, cwd, status: "waiting" })
|
|
221
|
+
} catch {
|
|
222
|
+
/* ignore */
|
|
223
|
+
}
|
|
224
|
+
}
|
|
191
225
|
client.send({ type: "claude.event", stream_id, session_id, event })
|
|
192
226
|
})
|
|
193
227
|
claudeBridge.on("permission", ({ stream_id, session_id, request_id, tool_name, input }) => {
|
package/src/service-install.mjs
CHANGED
|
@@ -128,6 +128,67 @@ function run(cmd, args, opts = {}) {
|
|
|
128
128
|
}
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* macOS の `launchctl bootout` は非同期で、graceful shutdown を持つ hub-agent は
|
|
135
|
+
* 実際に launchd の登録から消えるまでに時間がかかる。直後に `bootstrap` を叩くと
|
|
136
|
+
* 旧サービスがまだ抜け切っておらず `Bootstrap failed: 5: Input/output error` 等で
|
|
137
|
+
* 失敗し、plist はディスクにあるのにロードされない (= KeepAlive 不発 → offline)
|
|
138
|
+
* 状態に陥る。本関数は対象 service が launchd の登録から消えるまで polling して待つ。
|
|
139
|
+
* (2026-05-30: install-service の bootout→bootstrap レースで offline 化する事故の修正)
|
|
140
|
+
*
|
|
141
|
+
* @param {number} uid
|
|
142
|
+
* @param {{ timeoutMs?: number, intervalMs?: number }} [opts]
|
|
143
|
+
* @returns {Promise<boolean>} 消えたら true / timeout なら false
|
|
144
|
+
*/
|
|
145
|
+
async function waitUntilBootedOut(uid, opts = {}) {
|
|
146
|
+
const timeoutMs = opts.timeoutMs ?? 5000
|
|
147
|
+
const intervalMs = opts.intervalMs ?? 200
|
|
148
|
+
const spawn = opts.spawn ?? spawnSync
|
|
149
|
+
const sleepFn = opts.sleep ?? sleep
|
|
150
|
+
const now = opts.now ?? (() => Date.now())
|
|
151
|
+
const deadline = now() + timeoutMs
|
|
152
|
+
while (now() < deadline) {
|
|
153
|
+
const r = spawn("launchctl", ["print", `gui/${uid}/${PLIST_LABEL}`], {
|
|
154
|
+
stdio: "ignore",
|
|
155
|
+
})
|
|
156
|
+
// 非 0 = 登録に存在しない = bootout 完了
|
|
157
|
+
if (r.status !== 0) return true
|
|
158
|
+
await sleepFn(intervalMs)
|
|
159
|
+
}
|
|
160
|
+
return false
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* bootout 後の bootstrap を、過渡的失敗 (旧サービスの抜け残り等) に対して
|
|
165
|
+
* リトライする。`run` と違い最終的に失敗したときだけ throw する。
|
|
166
|
+
*
|
|
167
|
+
* @param {number} uid
|
|
168
|
+
* @param {string} dest plist パス
|
|
169
|
+
* @param {{ attempts?: number, intervalMs?: number }} [opts]
|
|
170
|
+
*/
|
|
171
|
+
async function bootstrapWithRetry(uid, dest, opts = {}) {
|
|
172
|
+
const attempts = opts.attempts ?? 4
|
|
173
|
+
const intervalMs = opts.intervalMs ?? 400
|
|
174
|
+
const spawn = opts.spawn ?? spawnSync
|
|
175
|
+
const sleepFn = opts.sleep ?? sleep
|
|
176
|
+
let lastStatus = null
|
|
177
|
+
for (let i = 0; i < attempts; i++) {
|
|
178
|
+
const r = spawn("launchctl", ["bootstrap", `gui/${uid}`, dest], {
|
|
179
|
+
stdio: "inherit",
|
|
180
|
+
})
|
|
181
|
+
if (r.status === 0) return
|
|
182
|
+
// 既にロード済み (status 37 / "service already loaded") は成功とみなす
|
|
183
|
+
if (r.status === 37) return
|
|
184
|
+
lastStatus = r.status
|
|
185
|
+
await sleepFn(intervalMs)
|
|
186
|
+
}
|
|
187
|
+
throw new Error(
|
|
188
|
+
`launchctl bootstrap gui/${uid} ${dest} failed after ${attempts} attempts (last exit ${lastStatus})`
|
|
189
|
+
)
|
|
190
|
+
}
|
|
191
|
+
|
|
131
192
|
/**
|
|
132
193
|
* `which hub-agent` の結果を、launchd / systemd から長期間 exec 可能な
|
|
133
194
|
* 安定パスに正規化する。pure 関数。`opts` で fs を差し替えてテスト可能。
|
|
@@ -200,10 +261,17 @@ export async function installService({ bin } = {}) {
|
|
|
200
261
|
await fs.writeFile(dest, expanded, { mode: 0o644 })
|
|
201
262
|
|
|
202
263
|
const uid = ensureUnixUid()
|
|
203
|
-
// 既存ロード解除 → bootstrap → kickstart
|
|
264
|
+
// 既存ロード解除 → (登録が消えるまで待機) → bootstrap (リトライ) → kickstart。
|
|
265
|
+
// bootout は非同期なので待たずに bootstrap するとレースで失敗し offline 化する
|
|
266
|
+
// (2026-05-30 の事故)。waitUntilBootedOut で確実に抜けてから bootstrap する。
|
|
204
267
|
spawnSync("launchctl", ["bootout", `gui/${uid}`, dest], { stdio: "ignore" })
|
|
205
|
-
|
|
206
|
-
|
|
268
|
+
await waitUntilBootedOut(uid)
|
|
269
|
+
await bootstrapWithRetry(uid, dest)
|
|
270
|
+
// kickstart は「既に走っている daemon を再起動」する用途。bootstrap + RunAtLoad で
|
|
271
|
+
// 既に起動済みのことがあり、その場合 kickstart 失敗は致命的でないので throw しない。
|
|
272
|
+
spawnSync("launchctl", ["kickstart", "-k", `gui/${uid}/${PLIST_LABEL}`], {
|
|
273
|
+
stdio: "ignore",
|
|
274
|
+
})
|
|
207
275
|
return { platform: "darwin", path: dest, label: PLIST_LABEL, bin: hubAgentBin }
|
|
208
276
|
}
|
|
209
277
|
|
|
@@ -300,4 +368,6 @@ export const _internal = {
|
|
|
300
368
|
macPlistPath,
|
|
301
369
|
linuxUnitPath,
|
|
302
370
|
repoTemplatesDir,
|
|
371
|
+
waitUntilBootedOut,
|
|
372
|
+
bootstrapWithRetry,
|
|
303
373
|
}
|
package/src/usage.mjs
CHANGED
|
@@ -124,9 +124,72 @@ function utilizationToPercent(u) {
|
|
|
124
124
|
return u <= 1 ? Math.round(u * 1000) / 10 : Math.round(u * 10) / 10
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
+
// persist の in-flight promise。テストやシャットダウンで書き込み完了を待てるよう保持。
|
|
128
|
+
let _persistInFlight = Promise.resolve()
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* 捕捉済みの chatRateLimits を statusLine cache (`~/.hub/usage/latest.json`) に
|
|
132
|
+
* マージ書き込みする。
|
|
133
|
+
*
|
|
134
|
+
* webapp 側 (`cockpit/webapp/lib/usage.ts`) のフッターは hub-agent の getUsage を
|
|
135
|
+
* 呼ばず、このファイルを直接読む (readOfficial)。そのファイルは TUI の statusLine
|
|
136
|
+
* でしか更新されないため、チャットモードでは 5h/7d が固まる。ここで rate_limit_event
|
|
137
|
+
* の実値をファイルへ書き戻すことで、webapp のフッターもチャット中にライブ更新される。
|
|
138
|
+
*
|
|
139
|
+
* 既存フィールド (context_window 等、TUI が書いた値) は保持し、rate_limits のみ
|
|
140
|
+
* 更新する。書き込みは tmp → rename の atomic write。全エラーは握りつぶす。
|
|
141
|
+
*/
|
|
142
|
+
async function persistChatRateLimitsToCache() {
|
|
143
|
+
const p = statuslineCache()
|
|
144
|
+
let base = {}
|
|
145
|
+
const existing = await readOrNull(p)
|
|
146
|
+
if (existing) {
|
|
147
|
+
try {
|
|
148
|
+
const parsed = JSON.parse(existing)
|
|
149
|
+
if (parsed && typeof parsed === "object") base = parsed
|
|
150
|
+
} catch {
|
|
151
|
+
/* 壊れたファイルは作り直す */
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
const rl =
|
|
155
|
+
base.rate_limits && typeof base.rate_limits === "object" ? { ...base.rate_limits } : {}
|
|
156
|
+
if (chatRateLimits.five_hour) {
|
|
157
|
+
rl.five_hour = {
|
|
158
|
+
used_percentage: chatRateLimits.five_hour.percent,
|
|
159
|
+
resets_at: chatRateLimits.five_hour.resetAtMs ?? rl.five_hour?.resets_at ?? null,
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
if (chatRateLimits.seven_day) {
|
|
163
|
+
rl.seven_day = {
|
|
164
|
+
used_percentage: chatRateLimits.seven_day.percent,
|
|
165
|
+
resets_at: chatRateLimits.seven_day.resetAtMs ?? rl.seven_day?.resets_at ?? null,
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
base.rate_limits = rl
|
|
169
|
+
try {
|
|
170
|
+
await fs.mkdir(path.dirname(p), { recursive: true })
|
|
171
|
+
} catch {
|
|
172
|
+
/* ignore */
|
|
173
|
+
}
|
|
174
|
+
const tmp = `${p}.tmp.${process.pid}`
|
|
175
|
+
try {
|
|
176
|
+
await fs.writeFile(tmp, JSON.stringify(base))
|
|
177
|
+
await fs.rename(tmp, p)
|
|
178
|
+
} catch {
|
|
179
|
+
try {
|
|
180
|
+
await fs.unlink(tmp)
|
|
181
|
+
} catch {
|
|
182
|
+
/* ignore */
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
127
187
|
/**
|
|
128
188
|
* SDK の SDKRateLimitInfo を取り込む (main.mjs の rate_limit_event ハンドラから呼ぶ)。
|
|
129
189
|
* rateLimitType: 'five_hour' / 'seven_day*' を 5h / 7d スロットに振り分ける。
|
|
190
|
+
*
|
|
191
|
+
* 取り込んだ値はプロセス内 (hub-agent の getUsage 用) と statusLine cache ファイル
|
|
192
|
+
* (webapp フッター用) の両方に反映する。ファイル書き込みは fire-and-forget。
|
|
130
193
|
*/
|
|
131
194
|
export function recordChatRateLimit(info) {
|
|
132
195
|
if (!info || typeof info !== "object") return
|
|
@@ -141,6 +204,141 @@ export function recordChatRateLimit(info) {
|
|
|
141
204
|
}
|
|
142
205
|
chatRateLimits[slot] = { percent, resetAtMs }
|
|
143
206
|
chatRateLimits.updatedAtMs = Date.now()
|
|
207
|
+
// webapp フッター (ファイルベース readOfficial) 用に latest.json へ書き戻す。
|
|
208
|
+
// fire-and-forget だが in-flight promise は保持 (flush 可能にする)。
|
|
209
|
+
_persistInFlight = persistChatRateLimitsToCache()
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* 直近の statusLine cache 書き込みの完了を待つ。テストや graceful shutdown 用。
|
|
214
|
+
* @returns {Promise<void>}
|
|
215
|
+
*/
|
|
216
|
+
export function whenChatRateLimitsPersisted() {
|
|
217
|
+
return _persistInFlight
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// session 別ファイル書き込みの in-flight promise (テスト/shutdown 用)。
|
|
221
|
+
// context% (assistant) と chat_status (result) の書き込みは read-modify-write の
|
|
222
|
+
// ため、この chain に直列化して lost-update / 競合を防ぐ。
|
|
223
|
+
let _sessionPersistInFlight = Promise.resolve()
|
|
224
|
+
|
|
225
|
+
/** session_id を安全なファイル名としてバリデーション (path traversal 防止)。 */
|
|
226
|
+
function isSafeSessionId(sid) {
|
|
227
|
+
return typeof sid === "string" && /^[A-Za-z0-9_-]{8,64}$/.test(sid)
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const VALID_CHAT_STATUS = new Set(["processing", "waiting", "idle"])
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* session 別ファイル (`~/.hub/usage/sessions/<sid>.json`) へ patch を
|
|
234
|
+
* マージ書き込みする (read-modify-write, atomic rename)。
|
|
235
|
+
*
|
|
236
|
+
* webapp サイドバーは getSessionUsages (context ドーナツ) と listSessions
|
|
237
|
+
* (ステータスドット) がこのディレクトリを読むが、ファイルは TUI statusLine
|
|
238
|
+
* でしか更新されないため、チャットモードでは両方が固まる。チャット中は SDK の
|
|
239
|
+
* usage / busy 状態からここへ書き出すことで両方ライブ更新される。
|
|
240
|
+
*
|
|
241
|
+
* context_window と chat_status は別タイミング (assistant / result) で書かれる
|
|
242
|
+
* ため、既存値を破壊しないよう deep merge する。
|
|
243
|
+
*/
|
|
244
|
+
async function mergeSessionFile(sessionId, cwd, patch) {
|
|
245
|
+
if (!isSafeSessionId(sessionId) || !cwd) return
|
|
246
|
+
const dir = statuslineSessionsDir()
|
|
247
|
+
const fp = path.join(dir, `${sessionId}.json`)
|
|
248
|
+
let base = {}
|
|
249
|
+
const existing = await readOrNull(fp)
|
|
250
|
+
if (existing) {
|
|
251
|
+
try {
|
|
252
|
+
const p = JSON.parse(existing)
|
|
253
|
+
if (p && typeof p === "object") base = p
|
|
254
|
+
} catch {
|
|
255
|
+
/* 壊れたファイルは作り直す */
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
const next = {
|
|
259
|
+
...base,
|
|
260
|
+
...patch,
|
|
261
|
+
session_id: sessionId,
|
|
262
|
+
workspace: { ...(base.workspace || {}), project_dir: cwd },
|
|
263
|
+
_source: "chat",
|
|
264
|
+
}
|
|
265
|
+
// context_window はネストオブジェクトなので明示マージ。
|
|
266
|
+
if (patch.context_window) {
|
|
267
|
+
next.context_window = { ...(base.context_window || {}), ...patch.context_window }
|
|
268
|
+
}
|
|
269
|
+
try {
|
|
270
|
+
await fs.mkdir(dir, { recursive: true })
|
|
271
|
+
} catch {
|
|
272
|
+
/* ignore */
|
|
273
|
+
}
|
|
274
|
+
const tmp = `${fp}.tmp.${process.pid}`
|
|
275
|
+
try {
|
|
276
|
+
await fs.writeFile(tmp, JSON.stringify(next))
|
|
277
|
+
await fs.rename(tmp, fp)
|
|
278
|
+
} catch {
|
|
279
|
+
try {
|
|
280
|
+
await fs.unlink(tmp)
|
|
281
|
+
} catch {
|
|
282
|
+
/* ignore */
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/** session 別ファイル書き込みを直列 chain に積む。 */
|
|
288
|
+
function enqueueSessionWrite(fn) {
|
|
289
|
+
_sessionPersistInFlight = _sessionPersistInFlight.catch(() => {}).then(fn)
|
|
290
|
+
return _sessionPersistInFlight
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* チャット assistant メッセージを取り込んで session 別 context% を更新する
|
|
295
|
+
* (main.mjs の event ハンドラから assistant メッセージで呼ぶ)。fire-and-forget。
|
|
296
|
+
*
|
|
297
|
+
* context トークン = input + cache_read + cache_creation + output(直近応答)。
|
|
298
|
+
* プロンプトキャッシュ有効時は cache_* に大半が乗るため 4 種を必ず合算する。
|
|
299
|
+
*
|
|
300
|
+
* @param {{ sessionId?: string, cwd?: string, message?: { usage?: object } }} arg
|
|
301
|
+
*/
|
|
302
|
+
export function recordChatSessionContext(arg) {
|
|
303
|
+
if (!arg || typeof arg !== "object") return
|
|
304
|
+
const { sessionId, cwd, message } = arg
|
|
305
|
+
const u = message?.usage
|
|
306
|
+
if (!u) return
|
|
307
|
+
const tokens =
|
|
308
|
+
(u.input_tokens || 0) +
|
|
309
|
+
(u.cache_read_input_tokens || 0) +
|
|
310
|
+
(u.cache_creation_input_tokens || 0) +
|
|
311
|
+
(u.output_tokens || 0)
|
|
312
|
+
if (tokens <= 0) return
|
|
313
|
+
enqueueSessionWrite(async () => {
|
|
314
|
+
const windowSize = await contextWindowSize()
|
|
315
|
+
const percent = Math.min(100, Math.round((tokens / windowSize) * 1000) / 10)
|
|
316
|
+
await mergeSessionFile(sessionId, cwd, { context_window: { used_percentage: percent } })
|
|
317
|
+
})
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* チャットのターン状態 (processing/waiting/idle) を session 別ファイルへ書き出す。
|
|
322
|
+
* main.mjs の event ハンドラから、assistant→processing / result→waiting で呼ぶ。
|
|
323
|
+
* webapp listSessions が tmux スクレイプの代わりにこれを優先する (チャットモードは
|
|
324
|
+
* ペインに TUI を出さずスクレイプが効かないため)。fire-and-forget。
|
|
325
|
+
*
|
|
326
|
+
* @param {{ sessionId?: string, cwd?: string, status?: string }} arg
|
|
327
|
+
*/
|
|
328
|
+
export function recordChatSessionStatus(arg) {
|
|
329
|
+
if (!arg || typeof arg !== "object") return
|
|
330
|
+
const { sessionId, cwd, status } = arg
|
|
331
|
+
if (!VALID_CHAT_STATUS.has(status)) return
|
|
332
|
+
enqueueSessionWrite(() => mergeSessionFile(sessionId, cwd, { chat_status: status }))
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* 直近の session 別ファイル書き込み (context%/status 両方) の完了を待つ。
|
|
337
|
+
* テスト/shutdown 用。
|
|
338
|
+
* @returns {Promise<void>}
|
|
339
|
+
*/
|
|
340
|
+
export function whenChatSessionContextPersisted() {
|
|
341
|
+
return _sessionPersistInFlight
|
|
144
342
|
}
|
|
145
343
|
|
|
146
344
|
async function readOfficial(now) {
|