@cocorograph/hub-agent 0.6.88 → 0.6.90
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-md-sync.mjs +5 -0
- package/src/claude-md.mjs +74 -7
- package/src/main.mjs +30 -5
- package/src/pty-bridge.mjs +11 -0
package/package.json
CHANGED
package/src/claude-md-sync.mjs
CHANGED
|
@@ -346,6 +346,11 @@ export async function syncClaudeMdWithHub({
|
|
|
346
346
|
return { action: "conflict" }
|
|
347
347
|
}
|
|
348
348
|
|
|
349
|
+
// メタ操作・ハッシュ計算は ensureClaudeMd 側 (Hub pull 初回配信) でも使うため
|
|
350
|
+
// named export する。同じファイルパス・同じスキーマで管理することで、初回配信と
|
|
351
|
+
// 以降の sync で食い違いを起こさない。
|
|
352
|
+
export { sha256, readMeta, writeMeta, nowIso }
|
|
353
|
+
|
|
349
354
|
// 内部テスト用エクスポート
|
|
350
355
|
export const _internals = {
|
|
351
356
|
sha256,
|
package/src/claude-md.mjs
CHANGED
|
@@ -2,17 +2,28 @@
|
|
|
2
2
|
* 新規 workspace ディレクトリ初期化時の CLAUDE.md 生成。
|
|
3
3
|
*
|
|
4
4
|
* ~/hub/bin/generate-claude-md (Python) と同等の仕様を Node 側に持つ。
|
|
5
|
-
* - `<dirName>/_director` slug で Hub director を引き当て、見つかれば
|
|
6
|
-
* frontmatter (client / domain / industry / service / tech_stack /
|
|
7
|
-
* google_drive) を反映した構造化 CLAUDE.md を書き出す
|
|
8
|
-
* - 認証トークン (`~/.claude/.hub_token.json`) が無い / API 失敗 / director
|
|
9
|
-
* 未登録ならプレースホルダ CLAUDE.md を書き出す
|
|
10
5
|
*
|
|
11
|
-
* CLAUDE.md
|
|
6
|
+
* 優先順 (新規生成時のみ。既存 CLAUDE.md は触らない):
|
|
7
|
+
* 1. **Hub の Project.claude_md 正本** (Stage 3 初回配信) — 非空ならそれを書く。
|
|
8
|
+
* 同期メタ (.cockpit-sync/claude-md.json) も初期化する。以降は
|
|
9
|
+
* syncClaudeMdWithHub が双方向同期する。
|
|
10
|
+
* 2. **Hub director frontmatter** — claude_md が空でも director があれば
|
|
11
|
+
* client/domain/industry/service/tech_stack/google_drive で構造化版を書く。
|
|
12
|
+
* 3. **placeholder** — token なし / API 失敗 / director 未登録なら最小プレースホルダ。
|
|
13
|
+
*
|
|
14
|
+
* 1 を最優先にしたのは、新規メンバーが workspace に入ったとき、組織で固まった
|
|
15
|
+
* CLAUDE.md(Stage 1/2 で正本化したもの)が即座に引かれて作業に取り掛かれる
|
|
16
|
+
* ようにするため。動的生成は「Hub に正本がまだ無いプロジェクト」の救済として
|
|
17
|
+
* のみ機能する。
|
|
12
18
|
*/
|
|
13
19
|
import fs from "node:fs/promises"
|
|
14
20
|
import os from "node:os"
|
|
15
21
|
import path from "node:path"
|
|
22
|
+
import {
|
|
23
|
+
sha256,
|
|
24
|
+
writeMeta,
|
|
25
|
+
nowIso,
|
|
26
|
+
} from "./claude-md-sync.mjs"
|
|
16
27
|
|
|
17
28
|
const DEFAULT_HUB_API = "https://api.hub.cocorograph.com"
|
|
18
29
|
const TOKEN_PATH = path.join(os.homedir(), ".claude", ".hub_token.json")
|
|
@@ -31,6 +42,34 @@ async function loadAccessToken() {
|
|
|
31
42
|
return (data && typeof data.access === "string" && data.access) || null
|
|
32
43
|
}
|
|
33
44
|
|
|
45
|
+
/**
|
|
46
|
+
* Hub の Project.claude_md 正本を取得する (Stage 3 初回配信用)。
|
|
47
|
+
*
|
|
48
|
+
* - 404 (該当 Project 無し) → null
|
|
49
|
+
* - その他のエラー → null
|
|
50
|
+
* - 取得成功時は { claude_md, claude_md_hash, claude_md_updated_at } を返す
|
|
51
|
+
*
|
|
52
|
+
* 同じエンドポイントは syncClaudeMdWithHub も使うが、ensureClaudeMd は
|
|
53
|
+
* 初回生成 (メタ無し) 経路でしか呼ばれず、PUT もしないため import せず
|
|
54
|
+
* 個別実装する。
|
|
55
|
+
*/
|
|
56
|
+
async function fetchHubClaudeMdProject({ hubUrl, accessToken, dirName, fetchImpl }) {
|
|
57
|
+
const f = fetchImpl || globalThis.fetch
|
|
58
|
+
if (!f) return null
|
|
59
|
+
const url =
|
|
60
|
+
`${hubUrl.replace(/\/+$/, "")}/api/admin/project/claude-md-by-dir/` +
|
|
61
|
+
`?dir_name=${encodeURIComponent(dirName)}`
|
|
62
|
+
try {
|
|
63
|
+
const res = await f(url, {
|
|
64
|
+
headers: { Authorization: `Bearer ${accessToken}` },
|
|
65
|
+
})
|
|
66
|
+
if (!res.ok) return null
|
|
67
|
+
return await res.json()
|
|
68
|
+
} catch {
|
|
69
|
+
return null
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
34
73
|
async function fetchDirector({ hubUrl, accessToken, dirName, fetchImpl }) {
|
|
35
74
|
const f = fetchImpl || globalThis.fetch
|
|
36
75
|
if (!f) return null
|
|
@@ -320,7 +359,35 @@ export async function ensureClaudeMd({
|
|
|
320
359
|
const token =
|
|
321
360
|
accessToken === undefined ? await loadAccessToken() : accessToken || null
|
|
322
361
|
const url = hubUrl || DEFAULT_HUB_API
|
|
323
|
-
|
|
362
|
+
|
|
363
|
+
// Stage 3: Hub に正本があればそれを最優先で書き込み、メタも初期化する
|
|
364
|
+
// (以降の syncClaudeMdWithHub が双方向同期の起点として使う)。
|
|
365
|
+
// Hub 側未保存 (claude_md_updated_at が null) なら従来パスへフォールバック。
|
|
366
|
+
if (token) {
|
|
367
|
+
const hub = await fetchHubClaudeMdProject({
|
|
368
|
+
hubUrl: url,
|
|
369
|
+
accessToken: token,
|
|
370
|
+
dirName,
|
|
371
|
+
fetchImpl,
|
|
372
|
+
})
|
|
373
|
+
const hubBody = hub?.claude_md || ""
|
|
374
|
+
const hubInitialized = !!hub?.claude_md_updated_at
|
|
375
|
+
if (hubInitialized && hubBody) {
|
|
376
|
+
await fs.writeFile(claudeMdPath, hubBody, "utf-8")
|
|
377
|
+
await writeMeta(targetDir, {
|
|
378
|
+
lastSyncedHubUpdatedAt: hub.claude_md_updated_at,
|
|
379
|
+
lastSyncedHash: hub.claude_md_hash || sha256(hubBody),
|
|
380
|
+
lastSyncedAt: nowIso(),
|
|
381
|
+
})
|
|
382
|
+
logger?.info?.(
|
|
383
|
+
{ dirName, claudeMdPath },
|
|
384
|
+
"wrote CLAUDE.md from Hub project claude_md (initial sync)",
|
|
385
|
+
)
|
|
386
|
+
return { written: true, source: "hub" }
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// 従来パス: director と repositories は独立して取れるので並列フェッチする。
|
|
324
391
|
// どちらも失敗時は null / [] にフォールバックする (token なしの場合も同様)。
|
|
325
392
|
const [director, repositories] = token
|
|
326
393
|
? await Promise.all([
|
package/src/main.mjs
CHANGED
|
@@ -1051,6 +1051,11 @@ export async function maybeSyncClaudeSettings(msg, ctx) {
|
|
|
1051
1051
|
}
|
|
1052
1052
|
}
|
|
1053
1053
|
|
|
1054
|
+
// Plan ε grace guard の猶予 (ms)。backend の集計タイムアウト (_STREAMS_SYNC_TIMEOUT_S=0.5s)
|
|
1055
|
+
// + browser の WS 再接続 + 再 attach を十分に超える値。これ以内に touch された stream は
|
|
1056
|
+
// 「再 attach 中で sync に間に合わなかっただけ」とみなして誤殺しない。10min GC より十分短い。
|
|
1057
|
+
const STREAMS_SYNC_GRACE_MS = 45_000
|
|
1058
|
+
|
|
1054
1059
|
/**
|
|
1055
1060
|
* Plan ε: `agent.streams.sync.response` を受けて backend ↔ agent の現存
|
|
1056
1061
|
* stream_id を能動同期する。
|
|
@@ -1066,14 +1071,34 @@ export function handleStreamsSyncResponse(msg, ctx) {
|
|
|
1066
1071
|
Array.isArray(msg?.active_stream_ids) ? msg.active_stream_ids : [],
|
|
1067
1072
|
)
|
|
1068
1073
|
const localIds = new Set(ctx.ptyBridge.list())
|
|
1074
|
+
// churn 退化スナップショット保護 (ack_lost 根治 / 2026-06-17 根因 rank3)。
|
|
1075
|
+
// backend は browser 群へ _STREAMS_SYNC_TIMEOUT_S(0.5s) だけ問い合わせて active set を
|
|
1076
|
+
// 集める。再接続バースト中 (本番 ~20s 間隔・code 1006) は browser がまだ再 attach できて
|
|
1077
|
+
// おらず空/部分的な set が返り、その一瞬を authoritative とみなして「ローカル全 stream が
|
|
1078
|
+
// 孤児」と誤判定し、生きている PTY を皆殺しにしていた (再 attach 直後の stream を 1 秒後に
|
|
1079
|
+
// 殺し、以後の入力が stream_missing → 数分単位の「送信待ち」固着。0.6.89 ログ最多 churn 源)。
|
|
1080
|
+
// 空 set は信用せず kill を一切行わない (真の孤児は 10min GC + maxStreams 上限が回収する)。
|
|
1081
|
+
const haveAuthoritativeActiveSet = activeIds.size > 0
|
|
1069
1082
|
for (const sid of localIds) {
|
|
1070
|
-
if (
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1083
|
+
if (activeIds.has(sid)) continue
|
|
1084
|
+
if (!haveAuthoritativeActiveSet) continue
|
|
1085
|
+
// grace guard: 直近 STREAMS_SYNC_GRACE_MS 以内に touch (attach/write/resize/pty 出力) された
|
|
1086
|
+
// stream は「browser が再接続中で 0.5s 集計に間に合わなかっただけ」の可能性が高い。猶予内は
|
|
1087
|
+
// 殺さず延命する。真に放置された孤児は猶予超過で従来どおり detach され、10min GC も backstop。
|
|
1088
|
+
const lastSeen = ctx.ptyBridge.getLastSeen?.(sid) ?? 0
|
|
1089
|
+
const idleMs = Date.now() - lastSeen
|
|
1090
|
+
if (lastSeen > 0 && idleMs < STREAMS_SYNC_GRACE_MS) {
|
|
1091
|
+
ctx.logger?.info?.(
|
|
1092
|
+
{ stream_id: sid, idle_ms: idleMs, request_id: msg?.request_id },
|
|
1093
|
+
"Plan ε: skip detach (within grace, browser likely re-attaching)",
|
|
1074
1094
|
)
|
|
1075
|
-
|
|
1095
|
+
continue
|
|
1076
1096
|
}
|
|
1097
|
+
ctx.logger?.warn?.(
|
|
1098
|
+
{ stream_id: sid, request_id: msg?.request_id },
|
|
1099
|
+
"Plan ε: orphan stream detected, detaching",
|
|
1100
|
+
)
|
|
1101
|
+
ctx.ptyBridge.detach({ stream_id: sid })
|
|
1077
1102
|
}
|
|
1078
1103
|
for (const sid of activeIds) {
|
|
1079
1104
|
if (!localIds.has(sid)) {
|
package/src/pty-bridge.mjs
CHANGED
|
@@ -169,6 +169,17 @@ export class PtyBridge extends EventEmitter {
|
|
|
169
169
|
this.lastSeenAt.set(stream_id, this._now())
|
|
170
170
|
}
|
|
171
171
|
|
|
172
|
+
/**
|
|
173
|
+
* stream の最終 touch 時刻 (ms epoch) を返す。未登録 / 未 touch は 0。
|
|
174
|
+
* Plan ε (handleStreamsSyncResponse) の grace 判定で「直近 attach/write/resize/出力が
|
|
175
|
+
* あった stream は browser が再 attach 中なだけ」と見なして誤殺を防ぐために使う。
|
|
176
|
+
* @param {string} stream_id
|
|
177
|
+
* @returns {number}
|
|
178
|
+
*/
|
|
179
|
+
getLastSeen(stream_id) {
|
|
180
|
+
return this.lastSeenAt.get(stream_id) ?? 0
|
|
181
|
+
}
|
|
182
|
+
|
|
172
183
|
/**
|
|
173
184
|
* `gcStaleMs` を超えて idle な stream を一括 reap する。
|
|
174
185
|
*
|