@cocorograph/hub-agent 0.7.12 → 0.7.14
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/chat-signals.mjs +41 -1
- package/src/claude-md-sync.mjs +28 -1
- package/src/claude-md.mjs +3 -0
- package/src/main.mjs +17 -1
package/package.json
CHANGED
package/src/chat-signals.mjs
CHANGED
|
@@ -35,6 +35,17 @@ const _byCwd = new Map()
|
|
|
35
35
|
// 一次対策で、本 TTL はその取りこぼし時のバックストップ。
|
|
36
36
|
const CHAT_SIGNAL_STALE_MS = 90 * 1000
|
|
37
37
|
|
|
38
|
+
// RC4 (症状④): stream_event (拡張思考の thinking_delta / 長時間ツール中の partial delta)
|
|
39
|
+
// 受信を status=processing の tick として記録する際の cwd 単位スロットル間隔。stream_event は
|
|
40
|
+
// トークン delta 毎に高頻度で流れるため、無制限に recordChatActivity を呼ぶと _byCwd への書き込み
|
|
41
|
+
// 洪水になる。この間隔で間引いても、>90s の拡張思考バースト / 単発ツールの間 statusAt・updatedAtMs
|
|
42
|
+
// を確実に前進させ、CHAT_SIGNAL_STALE_MS による idle 誤降格 (純 SDK チャットで三点リーダーが消える
|
|
43
|
+
// 事故) を防げる。
|
|
44
|
+
const STREAM_TICK_THROTTLE_MS = 3 * 1000
|
|
45
|
+
|
|
46
|
+
// cwd → 最後に stream tick を記録した時刻 (スロットル判定用)。
|
|
47
|
+
const _lastStreamTickAt = new Map()
|
|
48
|
+
|
|
38
49
|
const VALID_STATUS = new Set(["processing", "waiting", "idle"])
|
|
39
50
|
|
|
40
51
|
function _now() {
|
|
@@ -86,6 +97,33 @@ export function recordChatActivity(cwd, patch = {}, now = _now()) {
|
|
|
86
97
|
_byCwd.set(cwd, next)
|
|
87
98
|
}
|
|
88
99
|
|
|
100
|
+
/**
|
|
101
|
+
* RC4 (症状④): stream_event (拡張思考 thinking_delta / 長時間ツール中の partial delta) 受信を
|
|
102
|
+
* status=processing の「生きている」tick として記録する。assistant / result が流れない長い無音
|
|
103
|
+
* 区間 (>90s) でも statusAt・updatedAtMs を前進させ、getChatSignal が CHAT_SIGNAL_STALE_MS で
|
|
104
|
+
* null を返して三点リーダーが消える事故を防ぐ。
|
|
105
|
+
*
|
|
106
|
+
* 設計上の注意:
|
|
107
|
+
* - cwd 単位で STREAM_TICK_THROTTLE_MS に間引く (delta 毎の Map 書き込み洪水を回避)。
|
|
108
|
+
* - delta 本文は一切読まない (タイムスタンプ前進のみ)。拡張思考の本文は ephemeral で、ここで
|
|
109
|
+
* 保持・転送する必要は無く、プライバシー上も読むべきでない。
|
|
110
|
+
* - SDK のイベント順序上 stream_event はターン中の assistant ブロックに属し result より前に
|
|
111
|
+
* 流れる。abort 時は SDK query が即停止して onEvent が止まるため、result/turnsettled で
|
|
112
|
+
* waiting に落ちた後に trailing stream_event が processing を蘇生させる経路は無い。
|
|
113
|
+
*
|
|
114
|
+
* @param {string} cwd
|
|
115
|
+
* @param {number} [now]
|
|
116
|
+
*/
|
|
117
|
+
export function recordChatStreamTick(cwd, now = _now()) {
|
|
118
|
+
if (!cwd || typeof cwd !== "string") return
|
|
119
|
+
// 初回 (未 tick) は必ず記録する。`|| 0` で last を 0 既定にすると「時刻 0 で tick 済み」と
|
|
120
|
+
// 区別できず、初回 tick が now < throttle のとき誤って間引かれる。has で未 tick を判定する。
|
|
121
|
+
const last = _lastStreamTickAt.get(cwd)
|
|
122
|
+
if (last !== undefined && now - last < STREAM_TICK_THROTTLE_MS) return
|
|
123
|
+
_lastStreamTickAt.set(cwd, now)
|
|
124
|
+
recordChatActivity(cwd, { status: "processing" }, now)
|
|
125
|
+
}
|
|
126
|
+
|
|
89
127
|
/**
|
|
90
128
|
* cwd に対応する生きているチャット信号を返す。stale / 不在なら null。
|
|
91
129
|
*
|
|
@@ -116,11 +154,13 @@ export function getChatSignal(cwd, now = _now()) {
|
|
|
116
154
|
export function clearChatSignal(cwd) {
|
|
117
155
|
if (!cwd || typeof cwd !== "string") return
|
|
118
156
|
_byCwd.delete(cwd)
|
|
157
|
+
_lastStreamTickAt.delete(cwd)
|
|
119
158
|
}
|
|
120
159
|
|
|
121
160
|
/** テスト用: ストアを空にする。 */
|
|
122
161
|
export function _resetChatSignals() {
|
|
123
162
|
_byCwd.clear()
|
|
163
|
+
_lastStreamTickAt.clear()
|
|
124
164
|
}
|
|
125
165
|
|
|
126
|
-
export { CHAT_SIGNAL_STALE_MS }
|
|
166
|
+
export { CHAT_SIGNAL_STALE_MS, STREAM_TICK_THROTTLE_MS }
|
package/src/claude-md-sync.mjs
CHANGED
|
@@ -29,6 +29,7 @@ const DEFAULT_HUB_API = "https://api.hub.cocorograph.com"
|
|
|
29
29
|
const TOKEN_PATH = path.join(os.homedir(), ".claude", ".hub_token.json")
|
|
30
30
|
const META_DIR = ".cockpit-sync"
|
|
31
31
|
const META_FILE = "claude-md.json"
|
|
32
|
+
const BASE_FILE = "CLAUDE.md.base"
|
|
32
33
|
const BAK_KEEP = 3 // 通常 .bak と衝突 .bak それぞれ直近 3 件まで保持
|
|
33
34
|
|
|
34
35
|
async function readJsonOrNull(p) {
|
|
@@ -160,6 +161,23 @@ async function writeMeta(targetDir, meta) {
|
|
|
160
161
|
)
|
|
161
162
|
}
|
|
162
163
|
|
|
164
|
+
/**
|
|
165
|
+
* 同期成功時の本文を `.cockpit-sync/CLAUDE.md.base` に保存する。
|
|
166
|
+
*
|
|
167
|
+
* session-end スキル (対話中の Claude) が CLAUDE.md を Hub と 3-way merge する際の
|
|
168
|
+
* "base" 入力として使う。前回 sync 直後のローカル == Hub の本文を保持しておくことで、
|
|
169
|
+
* 「ローカル変更」と「Hub 変更」の差分を別々に抽出してから意味マージできる。
|
|
170
|
+
*
|
|
171
|
+
* meta(claude-md.json) は hash と updated_at しか持たないため、本文そのものが要る
|
|
172
|
+
* 3-way merge の用途ではこのファイルが必要。
|
|
173
|
+
*/
|
|
174
|
+
async function writeBaseBody(targetDir, body) {
|
|
175
|
+
if (body == null) return
|
|
176
|
+
const metaDir = path.join(targetDir, META_DIR)
|
|
177
|
+
await fs.mkdir(metaDir, { recursive: true })
|
|
178
|
+
await fs.writeFile(path.join(metaDir, BASE_FILE), body, "utf-8")
|
|
179
|
+
}
|
|
180
|
+
|
|
163
181
|
/**
|
|
164
182
|
* 既存ローカル CLAUDE.md を .cockpit-sync/CLAUDE.md.<prefix>.<ts>.bak に退避する。
|
|
165
183
|
* 同 prefix の .bak は新しい順に BAK_KEEP 件保持し、古いものから FIFO 削除する。
|
|
@@ -289,6 +307,8 @@ export async function syncClaudeMdWithHub({
|
|
|
289
307
|
lastSyncedHash: hubHash || sha256(hubBody),
|
|
290
308
|
lastSyncedAt: nowIso(),
|
|
291
309
|
})
|
|
310
|
+
// 3-way merge 用 base: pull 直後はローカル == Hub なので Hub 本文を base に。
|
|
311
|
+
await writeBaseBody(targetDir, hubBody)
|
|
292
312
|
logger?.info?.({ dirName }, "syncClaudeMdWithHub: pull")
|
|
293
313
|
return { action: "pull" }
|
|
294
314
|
}
|
|
@@ -315,6 +335,9 @@ export async function syncClaudeMdWithHub({
|
|
|
315
335
|
lastSyncedHash: localCurrentHash,
|
|
316
336
|
lastSyncedAt: nowIso(),
|
|
317
337
|
})
|
|
338
|
+
// 3-way merge 用 base: push 直後は Hub の正本がローカルと一致するので、
|
|
339
|
+
// 次回の base は今 push した本文 (= localBody)。
|
|
340
|
+
await writeBaseBody(targetDir, localBody || "")
|
|
318
341
|
logger?.info?.({ dirName }, "syncClaudeMdWithHub: push")
|
|
319
342
|
return { action: "push" }
|
|
320
343
|
}
|
|
@@ -339,6 +362,8 @@ export async function syncClaudeMdWithHub({
|
|
|
339
362
|
lastSyncedHash: localCurrentHash,
|
|
340
363
|
lastSyncedAt: nowIso(),
|
|
341
364
|
})
|
|
365
|
+
// 3-way merge 用 base: conflict push 後の Hub 正本は端末側本文 (= localBody)。
|
|
366
|
+
await writeBaseBody(targetDir, localBody || "")
|
|
342
367
|
logger?.warn?.(
|
|
343
368
|
{ dirName },
|
|
344
369
|
"syncClaudeMdWithHub: conflict (terminal wins); hub body saved to .bak.conflict",
|
|
@@ -349,13 +374,15 @@ export async function syncClaudeMdWithHub({
|
|
|
349
374
|
// メタ操作・ハッシュ計算は ensureClaudeMd 側 (Hub pull 初回配信) でも使うため
|
|
350
375
|
// named export する。同じファイルパス・同じスキーマで管理することで、初回配信と
|
|
351
376
|
// 以降の sync で食い違いを起こさない。
|
|
352
|
-
export { sha256, readMeta, writeMeta, nowIso }
|
|
377
|
+
export { sha256, readMeta, writeMeta, writeBaseBody, nowIso }
|
|
353
378
|
|
|
354
379
|
// 内部テスト用エクスポート
|
|
355
380
|
export const _internals = {
|
|
356
381
|
sha256,
|
|
357
382
|
readMeta,
|
|
358
383
|
writeMeta,
|
|
384
|
+
writeBaseBody,
|
|
359
385
|
backupLocalClaudeMd,
|
|
360
386
|
BAK_KEEP,
|
|
387
|
+
BASE_FILE,
|
|
361
388
|
}
|
package/src/claude-md.mjs
CHANGED
|
@@ -22,6 +22,7 @@ import path from "node:path"
|
|
|
22
22
|
import {
|
|
23
23
|
sha256,
|
|
24
24
|
writeMeta,
|
|
25
|
+
writeBaseBody,
|
|
25
26
|
nowIso,
|
|
26
27
|
} from "./claude-md-sync.mjs"
|
|
27
28
|
|
|
@@ -379,6 +380,8 @@ export async function ensureClaudeMd({
|
|
|
379
380
|
lastSyncedHash: hub.claude_md_hash || sha256(hubBody),
|
|
380
381
|
lastSyncedAt: nowIso(),
|
|
381
382
|
})
|
|
383
|
+
// 初回 pull 直後は base = Hub 本文。以降の session-end 3-way merge の入力。
|
|
384
|
+
await writeBaseBody(targetDir, hubBody)
|
|
382
385
|
logger?.info?.(
|
|
383
386
|
{ dirName, claudeMdPath },
|
|
384
387
|
"wrote CLAUDE.md from Hub project claude_md (initial sync)",
|
package/src/main.mjs
CHANGED
|
@@ -92,7 +92,12 @@ import {
|
|
|
92
92
|
getUsage,
|
|
93
93
|
recordChatRateLimit,
|
|
94
94
|
} from "./usage.mjs"
|
|
95
|
-
import {
|
|
95
|
+
import {
|
|
96
|
+
clearChatSignal,
|
|
97
|
+
getChatSignal,
|
|
98
|
+
recordChatActivity,
|
|
99
|
+
recordChatStreamTick,
|
|
100
|
+
} from "./chat-signals.mjs"
|
|
96
101
|
import {
|
|
97
102
|
ReadinessTracker,
|
|
98
103
|
busyChildCount,
|
|
@@ -491,6 +496,17 @@ export async function startDaemon({ version, ptyModule, claudeSdk } = {}) {
|
|
|
491
496
|
} catch {
|
|
492
497
|
/* ignore */
|
|
493
498
|
}
|
|
499
|
+
} else if (event?.type === "stream_event") {
|
|
500
|
+
// RC4 (症状④): 拡張思考 (thinking_delta) や長時間ツール中の partial delta では
|
|
501
|
+
// assistant / result が流れず statusAt / updatedAtMs が前進しないため、>90s の無音で
|
|
502
|
+
// getChatSignal が CHAT_SIGNAL_STALE_MS により null を返し、capture-pane が idle な
|
|
503
|
+
// 純 SDK チャットで三点リーダーが消える。stream_event 受信を throttle 付き processing
|
|
504
|
+
// tick として記録し statusAt を前進させる (本文は読まずタイムスタンプ前進のみ)。
|
|
505
|
+
try {
|
|
506
|
+
recordChatStreamTick(cwd)
|
|
507
|
+
} catch {
|
|
508
|
+
/* ignore */
|
|
509
|
+
}
|
|
494
510
|
}
|
|
495
511
|
client.send({ type: "claude.event", stream_id, session_id, event })
|
|
496
512
|
})
|