@cocorograph/hub-agent 0.7.17 → 0.7.18
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/main.mjs +19 -4
- package/src/tmux.mjs +4 -2
- package/src/usage.mjs +155 -36
package/package.json
CHANGED
package/src/main.mjs
CHANGED
|
@@ -87,6 +87,7 @@ import { TuiPermissionBridge } from "./tui-permission-bridge.mjs"
|
|
|
87
87
|
import { TuiViewerRegistry } from "./tui-viewer-registry.mjs"
|
|
88
88
|
import { JsonlLiveWatchers } from "./jsonl-live-watchers.mjs"
|
|
89
89
|
import {
|
|
90
|
+
boundSessionId,
|
|
90
91
|
contextWindowSize,
|
|
91
92
|
getSessionUsages,
|
|
92
93
|
getUsage,
|
|
@@ -662,6 +663,9 @@ export async function startDaemon({ version, ptyModule, claudeSdk } = {}) {
|
|
|
662
663
|
intervalMs: 5_000,
|
|
663
664
|
claudeBridge,
|
|
664
665
|
readinessTracker,
|
|
666
|
+
// tmux session_name → bind 中の session_id。turn_active を「そのペインが実際に走らせている
|
|
667
|
+
// セッションの jsonl」で判定するために渡す (cwd dir 内の別 jsonl 取り違え=メタ欠陥#1 の根治)。
|
|
668
|
+
boundSessions: ctx.tuiReboundSessions,
|
|
665
669
|
})
|
|
666
670
|
// bundle hook (cockpit_session_event_hook.py) が /tmp/cockpit_session_events/<name>.json
|
|
667
671
|
// に書き出す UserPromptSubmit / Stop / ready の event を fs.watch で拾って WS push する。
|
|
@@ -1188,7 +1192,7 @@ const STATE_TRACE = process.env.HUB_AGENT_STATE_TRACE === "1"
|
|
|
1188
1192
|
// TURN_STALL_WARN_MS と揃える。
|
|
1189
1193
|
const STALL_WARN_MS = Number(process.env.HUB_AGENT_STALL_WARN_MS ?? 90000)
|
|
1190
1194
|
|
|
1191
|
-
function startStateLoop({ client, plugins, logger, intervalMs, claudeBridge, readinessTracker }) {
|
|
1195
|
+
function startStateLoop({ client, plugins, logger, intervalMs, claudeBridge, readinessTracker, boundSessions }) {
|
|
1192
1196
|
const lastByName = new Map() // session_name → {status, context_pct}
|
|
1193
1197
|
const lastTurnAtByName = new Map() // session_name → 最後に event ファイル化した turnAt
|
|
1194
1198
|
// (i) session_name → {sig, changedAt}: 出力領域署名の最終変化時刻 (出力フロー検知用)。
|
|
@@ -1304,14 +1308,17 @@ function startStateLoop({ client, plugins, logger, intervalMs, claudeBridge, rea
|
|
|
1304
1308
|
// CockpitStatusDot の青/赤の権威ソース。サイドバーのドットと送信/停止ボタンを同じ真実で
|
|
1305
1309
|
// 同期させ、Esc 中断後の青固着 (PROMPT_STALE_SHORT_MS=60s) を解消する。
|
|
1306
1310
|
// - チャットモード: SDK の rate_limit_event/assistant が最も新鮮なので chat.status を採用
|
|
1307
|
-
// - TUI モード: jsonl 末尾 assistant.stop_reason から判定 (turnActiveForCwd)
|
|
1311
|
+
// - TUI モード: bound session の jsonl 末尾 assistant.stop_reason から判定 (turnActiveForCwd)
|
|
1312
|
+
// bound session_id を渡すことで、同一 cwd dir 内の別セッション / サブエージェント /
|
|
1313
|
+
// headless `claude -p` / `/clear` ローテの jsonl を「最新」と取り違えて turn_active を
|
|
1314
|
+
// 誤る (cross-activity contamination = メタ欠陥#1) のを防ぐ。
|
|
1308
1315
|
let turnActive = null
|
|
1309
1316
|
if (chat?.status === "processing") {
|
|
1310
1317
|
turnActive = true
|
|
1311
1318
|
} else if (chat?.status === "waiting" || chat?.status === "idle") {
|
|
1312
1319
|
turnActive = false
|
|
1313
1320
|
} else if (s.cwd) {
|
|
1314
|
-
turnActive = await turnActiveForCwd(s.cwd)
|
|
1321
|
+
turnActive = await turnActiveForCwd(s.cwd, boundSessionId(boundSessions, s.session_name))
|
|
1315
1322
|
}
|
|
1316
1323
|
|
|
1317
1324
|
const prev = lastByName.get(s.session_name)
|
|
@@ -1387,6 +1394,8 @@ function startStateLoop({ client, plugins, logger, intervalMs, claudeBridge, rea
|
|
|
1387
1394
|
lastByName.delete(name)
|
|
1388
1395
|
outputFlowByName.delete(name)
|
|
1389
1396
|
lastTurnAtByName.delete(name)
|
|
1397
|
+
// 死んだセッションの bind 記録も掃除する (同名再作成時は claude.tui.bind が再登録)。
|
|
1398
|
+
boundSessions?.delete(name)
|
|
1390
1399
|
}
|
|
1391
1400
|
}
|
|
1392
1401
|
}
|
|
@@ -2901,7 +2910,13 @@ async function dispatch(msg, ctx) {
|
|
|
2901
2910
|
}
|
|
2902
2911
|
case "tmux.list_sessions": {
|
|
2903
2912
|
try {
|
|
2904
|
-
|
|
2913
|
+
// boundSessions: 各行の context% (ドーナツ) を bound session の jsonl から引く
|
|
2914
|
+
// (cwd dir 内の別セッション jsonl を最新と取り違える=メタ欠陥#1 の根治)。
|
|
2915
|
+
const sessions = await listTmuxSessions({
|
|
2916
|
+
plugins: ctx.plugins,
|
|
2917
|
+
logger: ctx.logger,
|
|
2918
|
+
boundSessions: ctx.tuiReboundSessions,
|
|
2919
|
+
})
|
|
2905
2920
|
// 各 session の最新 hook event を /tmp/cockpit_session_events/ から読んで
|
|
2906
2921
|
// レスポンスに含める (Phase 4: ハードリロード時の lastEvent 復元用)。
|
|
2907
2922
|
// fs.watch 由来の session.event push は揮発性のため、frontend が新規
|
package/src/tmux.mjs
CHANGED
|
@@ -27,7 +27,7 @@ import {
|
|
|
27
27
|
detectSessionState,
|
|
28
28
|
getSessionCwd,
|
|
29
29
|
} from "./state.mjs"
|
|
30
|
-
import { getSessionUsages, jsonlContextForCwd } from "./usage.mjs"
|
|
30
|
+
import { boundSessionId, getSessionUsages, jsonlContextForCwd } from "./usage.mjs"
|
|
31
31
|
|
|
32
32
|
const execFileP = promisify(execFile)
|
|
33
33
|
|
|
@@ -642,7 +642,9 @@ export async function listSessions(opts = {}) {
|
|
|
642
642
|
// 3. statusLine が無ければ jsonl per-cwd の末尾 assistant.usage 由来 %。
|
|
643
643
|
// 4. それも無ければ pane scrape の正規表現フォールバック。
|
|
644
644
|
const fromStatusLine = cwd ? ctxByCwd.get(cwd)?.contextPercent : null
|
|
645
|
-
|
|
645
|
+
// bound session があればその jsonl で context% を引く (cwd dir の別セッション jsonl の取り違え回避)。
|
|
646
|
+
const boundId = boundSessionId(opts.boundSessions, s.name)
|
|
647
|
+
const jsonlInfo = cwd ? await jsonlContextForCwd(cwd, boundId) : null
|
|
646
648
|
const context_pct =
|
|
647
649
|
jsonlInfo?.isReset && typeof jsonlInfo.percent === "number"
|
|
648
650
|
? jsonlInfo.percent
|
package/src/usage.mjs
CHANGED
|
@@ -537,6 +537,72 @@ async function latestJsonlContext(now) {
|
|
|
537
537
|
*/
|
|
538
538
|
const _jsonlSessionUuidByCwd = new Map()
|
|
539
539
|
|
|
540
|
+
/**
|
|
541
|
+
* cwd の jsonl ディレクトリから「対象セッションの jsonl 1 本」を解決する。
|
|
542
|
+
*
|
|
543
|
+
* bound `sessionId` が与えられ、その `<sessionId>.jsonl` が実在すれば **最優先で採用**する
|
|
544
|
+
* (= その tmux ペインが実際に bind/resume して走らせているセッション)。無ければ従来どおり
|
|
545
|
+
* mtime 最新へフォールバックする。
|
|
546
|
+
*
|
|
547
|
+
* 狙い: 1 つの cwd-encode dir には過去の全セッション / サブエージェント / headless `claude -p`
|
|
548
|
+
* (記憶蒸留 Stop hook 等) / `/clear` ローテの jsonl が堆積する (実測 52〜144 本)。mtime 最新を
|
|
549
|
+
* 「そのセッション」とみなすと、別アクティビティの jsonl が最新化した瞬間に turn_active /
|
|
550
|
+
* context% を取り違える (cross-activity contamination)。bound session_id 解決でこれを断つ。
|
|
551
|
+
*
|
|
552
|
+
* @param {string} dir cwd を encode した projects 配下のディレクトリ
|
|
553
|
+
* @param {string[]} files dir 内のファイル名一覧 (呼び出し側が readdir 済み)
|
|
554
|
+
* @param {string|null} sessionId その tmux ペインが bind しているセッション id (任意)
|
|
555
|
+
* @returns {Promise<{fp:string, mtimeMs:number, size:number, uuid:string, bound:boolean}|null>}
|
|
556
|
+
*/
|
|
557
|
+
async function resolveTargetJsonl(dir, files, sessionId) {
|
|
558
|
+
if (typeof sessionId === "string" && sessionId) {
|
|
559
|
+
const want = `${sessionId}.jsonl`
|
|
560
|
+
if (files.includes(want)) {
|
|
561
|
+
const fp = path.join(dir, want)
|
|
562
|
+
const st = await fs.stat(fp).catch(() => null)
|
|
563
|
+
if (st) {
|
|
564
|
+
return { fp, mtimeMs: st.mtimeMs, size: st.size, uuid: sessionId, bound: true }
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
// フォールバック: bound 未指定 / その jsonl が未だ存在しない (起動直後など) → mtime 最新。
|
|
569
|
+
let newest = null
|
|
570
|
+
await Promise.all(
|
|
571
|
+
files.map(async (f) => {
|
|
572
|
+
if (!f.endsWith(".jsonl")) return
|
|
573
|
+
const fp = path.join(dir, f)
|
|
574
|
+
const st = await fs.stat(fp).catch(() => null)
|
|
575
|
+
if (!st) return
|
|
576
|
+
if (!newest || st.mtimeMs > newest.mtimeMs) {
|
|
577
|
+
newest = {
|
|
578
|
+
fp,
|
|
579
|
+
mtimeMs: st.mtimeMs,
|
|
580
|
+
size: st.size,
|
|
581
|
+
uuid: f.replace(/\.jsonl$/, ""),
|
|
582
|
+
bound: false,
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
}),
|
|
586
|
+
)
|
|
587
|
+
return newest
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
/**
|
|
591
|
+
* `tuiReboundSessions` (tmux session_name → session_id or "fresh:<req>") の値から、
|
|
592
|
+
* 実セッション id を取り出す。fresh プレースホルダ / 未 bind は null。
|
|
593
|
+
* turn_active / context% 解決の呼び出し側 (state loop / listSessions) が、cwd でなく
|
|
594
|
+
* 「そのペインが実際に走らせているセッション」で jsonl を引くために使う。
|
|
595
|
+
*
|
|
596
|
+
* @param {Map<string,string>|null|undefined} boundSessions
|
|
597
|
+
* @param {string} name tmux セッション名
|
|
598
|
+
* @returns {string|null}
|
|
599
|
+
*/
|
|
600
|
+
export function boundSessionId(boundSessions, name) {
|
|
601
|
+
if (!boundSessions || !name) return null
|
|
602
|
+
const v = boundSessions.get(name)
|
|
603
|
+
return typeof v === "string" && v && !v.startsWith("fresh:") ? v : null
|
|
604
|
+
}
|
|
605
|
+
|
|
540
606
|
/**
|
|
541
607
|
* 指定 cwd の Claude セッション jsonl から context% (USED %) を算出する。
|
|
542
608
|
*
|
|
@@ -563,7 +629,7 @@ const _jsonlSessionUuidByCwd = new Map()
|
|
|
563
629
|
* @param {string} cwd
|
|
564
630
|
* @returns {Promise<{ percent: number | null, uuid: string | null, isReset: boolean }>}
|
|
565
631
|
*/
|
|
566
|
-
export async function jsonlContextForCwd(cwd) {
|
|
632
|
+
export async function jsonlContextForCwd(cwd, sessionId = null) {
|
|
567
633
|
const empty = { percent: null, uuid: null, isReset: false }
|
|
568
634
|
if (!cwd) return empty
|
|
569
635
|
const dirName = encodeCwdToDirName(cwd)
|
|
@@ -572,23 +638,8 @@ export async function jsonlContextForCwd(cwd) {
|
|
|
572
638
|
const files = await fs.readdir(dir).catch(() => null)
|
|
573
639
|
if (!files) return empty
|
|
574
640
|
|
|
575
|
-
|
|
576
|
-
await
|
|
577
|
-
files.map(async (f) => {
|
|
578
|
-
if (!f.endsWith(".jsonl")) return
|
|
579
|
-
const fp = path.join(dir, f)
|
|
580
|
-
const st = await fs.stat(fp).catch(() => null)
|
|
581
|
-
if (!st) return
|
|
582
|
-
if (!newest || st.mtimeMs > newest.mtimeMs) {
|
|
583
|
-
newest = {
|
|
584
|
-
fp,
|
|
585
|
-
mtimeMs: st.mtimeMs,
|
|
586
|
-
size: st.size,
|
|
587
|
-
uuid: f.replace(/\.jsonl$/, ""),
|
|
588
|
-
}
|
|
589
|
-
}
|
|
590
|
-
}),
|
|
591
|
-
)
|
|
641
|
+
// bound session_id があればその jsonl を、無ければ mtime 最新を採用 (cross-activity 汚染回避)。
|
|
642
|
+
const newest = await resolveTargetJsonl(dir, files, sessionId)
|
|
592
643
|
if (!newest) return empty
|
|
593
644
|
|
|
594
645
|
// 末尾 assistant.usage を探す。`_jsonlCtxMemo` は fp+mtime+size をキーにしているので
|
|
@@ -637,8 +688,11 @@ export async function jsonlContextForCwd(cwd) {
|
|
|
637
688
|
})
|
|
638
689
|
}
|
|
639
690
|
|
|
640
|
-
|
|
641
|
-
|
|
691
|
+
// isReset 検知のキーは bound session があればそれ、無ければ cwd。同一 cwd を複数の tmux
|
|
692
|
+
// セッションが共有しても、bound 時は各セッション独立に /clear (uuid 変化) を検知できる。
|
|
693
|
+
const trackKey = sessionId || cwd
|
|
694
|
+
const prevUuid = _jsonlSessionUuidByCwd.get(trackKey)
|
|
695
|
+
_jsonlSessionUuidByCwd.set(trackKey, newest.uuid)
|
|
642
696
|
const uuidChanged = prevUuid !== undefined && prevUuid !== newest.uuid
|
|
643
697
|
|
|
644
698
|
if (percent == null) {
|
|
@@ -665,6 +719,38 @@ const TURN_END_STOP_REASONS = new Set([
|
|
|
665
719
|
"max_tokens",
|
|
666
720
|
])
|
|
667
721
|
|
|
722
|
+
// 末尾が実ユーザー入力 (応答未着) でも、その入力がこの時間より古ければ「中断/クラッシュ/送信
|
|
723
|
+
// 直後終了で応答が来ないまま終わったセッション」とみなし active=false に倒す。frontend
|
|
724
|
+
// events.ts の STALE_USER_TAIL_MS と揃える。ステータスドット 0.7.17 一本化で agent 側に
|
|
725
|
+
// 移植漏れた鮮度フロアで、Esc 中断後に放置した jsonl 等が永久に turn_active=true (青) に
|
|
726
|
+
// 居座り 0.7.17 の「赤=閉じろナッジ」を相殺していた問題を根治する。
|
|
727
|
+
// ※ tool_use 末尾 (assistant が active を立てたケース) は floor しない (frontend と同じ。
|
|
728
|
+
// 長時間ツール実行を誤って畳まない)。ツール中クラッシュは PID 生存判定が別途必要。
|
|
729
|
+
const STALE_USER_TAIL_MS = 60_000
|
|
730
|
+
|
|
731
|
+
/** jsonl の user 行から表示テキスト (string content / text ブロック連結) を取り出す。
|
|
732
|
+
* コマンド echo (`<command-name>`) 判定に使う。 */
|
|
733
|
+
function userEventText(d) {
|
|
734
|
+
const content = d?.message?.content
|
|
735
|
+
if (typeof content === "string") return content
|
|
736
|
+
if (Array.isArray(content)) {
|
|
737
|
+
return content
|
|
738
|
+
.map((c) => (c && c.type === "text" ? String(c.text || "") : ""))
|
|
739
|
+
.join("")
|
|
740
|
+
}
|
|
741
|
+
return ""
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
/** ISO 文字列 / 数値の timestamp を ms に。取れなければ null。 */
|
|
745
|
+
function parseEntryTs(ts) {
|
|
746
|
+
if (typeof ts === "number") return Number.isFinite(ts) ? ts : null
|
|
747
|
+
if (typeof ts === "string") {
|
|
748
|
+
const n = Date.parse(ts)
|
|
749
|
+
return Number.isFinite(n) ? n : null
|
|
750
|
+
}
|
|
751
|
+
return null
|
|
752
|
+
}
|
|
753
|
+
|
|
668
754
|
/**
|
|
669
755
|
* jsonl の末尾を順走査してターンが現在 active かを返す。
|
|
670
756
|
*
|
|
@@ -679,13 +765,24 @@ const TURN_END_STOP_REASONS = new Set([
|
|
|
679
765
|
* 初期値は「ターン完了状態」(lastTurnEnded=true)。空 jsonl や応答未着 のセッション
|
|
680
766
|
* は active=false でフォールバックする。
|
|
681
767
|
*
|
|
768
|
+
* 鮮度フロア: 最終的な active=true が「実ユーザー入力」由来 (応答未着) で、その入力が
|
|
769
|
+
* STALE_USER_TAIL_MS より古ければ false に倒す (中断/クラッシュ/放置の永久青を解消)。
|
|
770
|
+
* assistant の tool_use 由来 active は floor しない (長時間ツール実行を畳まない)。
|
|
771
|
+
* コマンド echo (`<command-name>` 注入: /clear /compact /skill 等) は LLM ターンを起こさない
|
|
772
|
+
* 行があるため、実ユーザー入力とみなさずスキップする (frontend isCommandEchoEvent skip と同等)。
|
|
773
|
+
*
|
|
682
774
|
* @param {string} text jsonl の (末尾) テキスト
|
|
775
|
+
* @param {number} [now] テスト用の現在時刻 (ms)。省略時は Date.now()
|
|
683
776
|
* @returns {boolean}
|
|
684
777
|
*/
|
|
685
|
-
export function scanTurnActive(text) {
|
|
778
|
+
export function scanTurnActive(text, now = Date.now()) {
|
|
686
779
|
if (!text) return false
|
|
687
780
|
let active = false
|
|
688
781
|
let lastTurnEnded = true
|
|
782
|
+
// 最終 active=true を立てた要因 ("user"=実入力 / "assistant"=tool_use) と、その入力の時刻。
|
|
783
|
+
// 鮮度フロアは "user" 由来かつ古いときだけ適用する。
|
|
784
|
+
let activeBy = null
|
|
785
|
+
let activeSinceTs = null
|
|
689
786
|
const lines = text.split("\n")
|
|
690
787
|
for (const line of lines) {
|
|
691
788
|
if (!line) continue
|
|
@@ -700,10 +797,14 @@ export function scanTurnActive(text) {
|
|
|
700
797
|
if (stop && TURN_END_STOP_REASONS.has(stop)) {
|
|
701
798
|
active = false
|
|
702
799
|
lastTurnEnded = true
|
|
800
|
+
activeBy = null
|
|
801
|
+
activeSinceTs = null
|
|
703
802
|
} else {
|
|
704
803
|
// tool_use / pause_turn / null / 未知 → ターン継続中扱い (安全側)
|
|
705
804
|
active = true
|
|
706
805
|
lastTurnEnded = false
|
|
806
|
+
activeBy = "assistant"
|
|
807
|
+
activeSinceTs = parseEntryTs(d.timestamp)
|
|
707
808
|
}
|
|
708
809
|
} else if (d.type === "user") {
|
|
709
810
|
// 「実ユーザー入力」と「tool_result の自動 user ラッパー」を区別する。
|
|
@@ -712,12 +813,38 @@ export function scanTurnActive(text) {
|
|
|
712
813
|
// 来たら新ターン開始 → active=true。
|
|
713
814
|
const content = Array.isArray(d.message?.content) ? d.message.content : []
|
|
714
815
|
const isToolResult = content.some((c) => c?.type === "tool_result")
|
|
715
|
-
if (
|
|
816
|
+
if (isToolResult) continue
|
|
817
|
+
const userText = userEventText(d)
|
|
818
|
+
// 中断マーカー ([Request interrupted by user...]) = ユーザーが Esc 等でターンを止めた。
|
|
819
|
+
// 直前が tool_use (active) でも明示的に完了扱いにする (= 生成は止まっている)。これが無いと
|
|
820
|
+
// ツール実行中の中断後に放置した jsonl が永久に active=true (青) に居座る。
|
|
821
|
+
if (userText.trim().startsWith("[Request interrupted by user")) {
|
|
822
|
+
active = false
|
|
823
|
+
lastTurnEnded = true
|
|
824
|
+
activeBy = null
|
|
825
|
+
activeSinceTs = null
|
|
826
|
+
continue
|
|
827
|
+
}
|
|
828
|
+
// コマンド echo (/clear /compact /skill 等) は LLM ターンを起こさない行があるため、
|
|
829
|
+
// 「実ユーザー入力」とみなさずスキップする (直前の会話状態を保つ)。
|
|
830
|
+
if (/<command-name>/.test(userText)) continue
|
|
831
|
+
if (lastTurnEnded) {
|
|
716
832
|
active = true
|
|
717
833
|
lastTurnEnded = false
|
|
834
|
+
activeBy = "user"
|
|
835
|
+
activeSinceTs = parseEntryTs(d.timestamp)
|
|
718
836
|
}
|
|
719
837
|
}
|
|
720
838
|
}
|
|
839
|
+
// 鮮度フロア: 実ユーザー入力で active になったまま応答が来ず、その入力が古ければ idle に倒す。
|
|
840
|
+
if (
|
|
841
|
+
active &&
|
|
842
|
+
activeBy === "user" &&
|
|
843
|
+
activeSinceTs !== null &&
|
|
844
|
+
now - activeSinceTs > STALE_USER_TAIL_MS
|
|
845
|
+
) {
|
|
846
|
+
return false
|
|
847
|
+
}
|
|
721
848
|
return active
|
|
722
849
|
}
|
|
723
850
|
|
|
@@ -732,14 +859,16 @@ const _turnActiveMemo = new Map()
|
|
|
732
859
|
* 同じ真実で同期させ、Esc 中断後の「青固着 60s」を解消する。
|
|
733
860
|
*
|
|
734
861
|
* 動作:
|
|
735
|
-
* 1. cwd → encoded dir →
|
|
862
|
+
* 1. cwd → encoded dir → bound `sessionId` の jsonl (あれば最優先) / 無ければ mtime 最新を選ぶ
|
|
736
863
|
* 2. 末尾 256KB を tail 読みし `scanTurnActive` でターン状態を判定
|
|
737
864
|
* 3. tail で結論が出ない巨大ツール結果ケースのみ全文 read にフォールバック
|
|
738
865
|
*
|
|
739
866
|
* @param {string} cwd
|
|
867
|
+
* @param {string|null} [sessionId] その tmux ペインが bind しているセッション id。指定時はその
|
|
868
|
+
* jsonl を直接読み、cwd dir 内の別セッション/サブエージェント/headless jsonl の取り違えを防ぐ。
|
|
740
869
|
* @returns {Promise<boolean | null>} true=生成中 / false=待機中 / null=jsonl 不在
|
|
741
870
|
*/
|
|
742
|
-
export async function turnActiveForCwd(cwd) {
|
|
871
|
+
export async function turnActiveForCwd(cwd, sessionId = null) {
|
|
743
872
|
if (!cwd) return null
|
|
744
873
|
const dirName = encodeCwdToDirName(cwd)
|
|
745
874
|
if (!dirName) return null
|
|
@@ -747,18 +876,8 @@ export async function turnActiveForCwd(cwd) {
|
|
|
747
876
|
const files = await fs.readdir(dir).catch(() => null)
|
|
748
877
|
if (!files) return null
|
|
749
878
|
|
|
750
|
-
|
|
751
|
-
await
|
|
752
|
-
files.map(async (f) => {
|
|
753
|
-
if (!f.endsWith(".jsonl")) return
|
|
754
|
-
const fp = path.join(dir, f)
|
|
755
|
-
const st = await fs.stat(fp).catch(() => null)
|
|
756
|
-
if (!st) return
|
|
757
|
-
if (!newest || st.mtimeMs > newest.mtimeMs) {
|
|
758
|
-
newest = { fp, mtimeMs: st.mtimeMs, size: st.size }
|
|
759
|
-
}
|
|
760
|
-
}),
|
|
761
|
-
)
|
|
879
|
+
// bound session_id があればその jsonl を、無ければ mtime 最新を採用 (cross-activity 汚染回避)。
|
|
880
|
+
const newest = await resolveTargetJsonl(dir, files, sessionId)
|
|
762
881
|
if (!newest) return null
|
|
763
882
|
|
|
764
883
|
const memo = _turnActiveMemo.get(newest.fp)
|