therookie 0.4.30 → 0.4.32
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/cli.js +3 -0
- package/src/commands/doctor.js +31 -7
- package/templates/hook/bin/recall-push.sh +22 -12
- package/version.json +1 -1
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -111,6 +111,9 @@ const VALUE_FLAGS = new Set([
|
|
|
111
111
|
'projectId',
|
|
112
112
|
'repo-path',
|
|
113
113
|
'repo-url',
|
|
114
|
+
// spec 2026-09-08 §2.4 — `rookie save --session <id>`. 화이트리스트에 없으면 값이 안 붙어
|
|
115
|
+
// flags.session === true 가 되고 저장 마커가 조용히 안 남는다(실측으로 잡힌 버그).
|
|
116
|
+
'session',
|
|
114
117
|
])
|
|
115
118
|
|
|
116
119
|
export function parseFlags(args) {
|
package/src/commands/doctor.js
CHANGED
|
@@ -460,28 +460,44 @@ function printOrphanPending() {
|
|
|
460
460
|
// 서버 전송 없음. 파일이 없으면(아직 안 돌았거나 구버전) 침묵한다.
|
|
461
461
|
export function summarizeRecallPush(lines, max = 1000) {
|
|
462
462
|
const rows = []
|
|
463
|
+
// spec 2026-09-08 훅 타임아웃 유실 — 시작 줄(phase:'start')과 완료 줄을 turn 으로 짝지어,
|
|
464
|
+
// 짝 없는 시작이 곧 "훅이 제한 시간을 넘겨 통째로 죽은 턴"이다. 유실은 무주입과 다르다 —
|
|
465
|
+
// 무주입은 넣을 게 없던 것이고 유실은 넣으려다 못 넣은 것이라, 무주입률에 섞지 않는다.
|
|
466
|
+
const startTurns = new Set()
|
|
467
|
+
const endTurns = new Set()
|
|
463
468
|
// 파일은 append 전용이라 최신이 뒤에 온다 — 뒤에서부터 max 줄만 집계한다.
|
|
464
469
|
for (let i = lines.length - 1; i >= 0 && rows.length < max; i--) {
|
|
465
470
|
try {
|
|
466
471
|
const o = JSON.parse(lines[i])
|
|
467
|
-
if (o
|
|
472
|
+
if (!o) continue
|
|
473
|
+
if (o.phase === 'start') {
|
|
474
|
+
if (o.turn) startTurns.add(o.turn)
|
|
475
|
+
continue
|
|
476
|
+
}
|
|
477
|
+
if (o.turn) endTurns.add(o.turn)
|
|
478
|
+
// 구버전 줄에는 phase 가 없다 — 완료 줄로 읽는다(하위호환).
|
|
479
|
+
if (typeof o.injected === 'number') rows.push(o)
|
|
468
480
|
} catch {
|
|
469
481
|
/* 깨진 줄은 건너뛴다 */
|
|
470
482
|
}
|
|
471
483
|
}
|
|
472
|
-
|
|
484
|
+
let lost = 0
|
|
485
|
+
for (const t of startTurns) if (!endTurns.has(t)) lost++
|
|
486
|
+
if (rows.length === 0 && lost === 0) return null
|
|
473
487
|
const silent = rows.filter((r) => r.injected === 0).length
|
|
474
488
|
// 중앙값은 chars 기준이다 — 컨텍스트 예산은 문자·토큰 감각이고, 한국어는 바이트가 3배라
|
|
475
489
|
// 바이트로 재면 합격 기준(§4.1)이 왜곡된다. bytes 는 파일에만 남긴다.
|
|
476
490
|
const chars = rows.map((r) => (typeof r.chars === 'number' ? r.chars : 0)).sort((a, b) => a - b)
|
|
477
|
-
|
|
491
|
+
// 완료 줄이 하나도 없는 창(시작 줄만 남은 경우)에서 undefined 가 새어 나가지 않게 0 으로 닫는다.
|
|
492
|
+
const median = chars.length > 0 ? chars[Math.floor(chars.length / 2)] : 0
|
|
478
493
|
const epochs = new Set(rows.map((r) => String(r.epoch ?? '')))
|
|
479
494
|
return {
|
|
480
495
|
turns: rows.length,
|
|
481
496
|
silent,
|
|
482
|
-
silentRatio: silent / rows.length,
|
|
497
|
+
silentRatio: rows.length > 0 ? silent / rows.length : 0,
|
|
483
498
|
medianChars: median,
|
|
484
499
|
epochs: epochs.size,
|
|
500
|
+
lost,
|
|
485
501
|
}
|
|
486
502
|
}
|
|
487
503
|
|
|
@@ -502,9 +518,17 @@ function printRecallPush() {
|
|
|
502
518
|
}
|
|
503
519
|
if (!s) return
|
|
504
520
|
const pct = Math.round(s.silentRatio * 100)
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
521
|
+
if (s.turns > 0) {
|
|
522
|
+
console.log(
|
|
523
|
+
`— 회상 푸시: 최근 ${s.turns}턴 중 무주입 ${s.silent}건(${pct}%), 주입 길이 중앙값 ${s.medianChars}자, epoch ${s.epochs}개`
|
|
524
|
+
)
|
|
525
|
+
}
|
|
526
|
+
// 유실 0 이면 침묵 — 정상 상태를 매번 알리지 않는다.
|
|
527
|
+
if (s.lost > 0) {
|
|
528
|
+
console.log(
|
|
529
|
+
`⚠️ 훅 유실: 회상 ${s.lost}건 — 훅이 제한 시간을 넘겨 주입이 버려졌습니다. 부하가 높았는지 확인하세요.`
|
|
530
|
+
)
|
|
531
|
+
}
|
|
508
532
|
}
|
|
509
533
|
|
|
510
534
|
// B6-3 (spec 2026-07-11 §B6): 세션 identity pinning 위반 가시화 — 세션 도중 계정 전환으로
|
|
@@ -55,15 +55,14 @@ process.stdin.on("data",c=>d+=c).on("end",async()=>{
|
|
|
55
55
|
// 병렬 세션 안전: 쓰기는 append 전용(O_APPEND, 한 줄은 원자적)이고, 크기 초과 시에만
|
|
56
56
|
// rename 으로 로테이트한다. 파일을 읽어 다시 쓰는 방식은 두 세션이 같은 스냅샷을 보고
|
|
57
57
|
// 서로의 줄을 덮어쓸 수 있어 쓰지 않는다(codex 코드검토 R1 P1).
|
|
58
|
-
|
|
58
|
+
// spec 2026-09-08 훅 타임아웃 유실 — 시작 줄과 완료 줄을 turn 으로 짝지어, 짝 없는 시작이
|
|
59
|
+
// 곧 "프로세스가 통째로 죽은 턴"이 된다. 시작 줄은 node 진입 직후에만 쓴다(집계 범위 = node 단계).
|
|
60
|
+
const turn=crypto.randomBytes(4).toString("hex");
|
|
61
|
+
const writeLine=(obj)=>{
|
|
59
62
|
try{
|
|
60
63
|
const dir=process.env.ROOKIE_PUSH_DIR;
|
|
61
64
|
const f=path.join(dir,"metrics.jsonl");
|
|
62
|
-
const
|
|
63
|
-
const line=JSON.stringify({
|
|
64
|
-
ts:new Date().toISOString(),epoch,layer,candidates,injected,
|
|
65
|
-
chars:s.length,bytes:Buffer.byteLength(s)
|
|
66
|
-
})+"\n";
|
|
65
|
+
const line=JSON.stringify({ts:new Date().toISOString(),epoch,layer:"B",turn,...obj})+"\n";
|
|
67
66
|
fs.mkdirSync(dir,{recursive:true,mode:0o700});
|
|
68
67
|
try{
|
|
69
68
|
if(fs.statSync(f).size>METRIC_MAX_BYTES) fs.renameSync(f,f+".1");
|
|
@@ -71,11 +70,18 @@ process.stdin.on("data",c=>d+=c).on("end",async()=>{
|
|
|
71
70
|
fs.appendFileSync(f,line,{mode:0o600});
|
|
72
71
|
}catch{}
|
|
73
72
|
};
|
|
73
|
+
const metricStart=()=>writeLine({phase:"start"});
|
|
74
|
+
// 종료 경로마다 한 번. reason 은 코드의 return·catch 와 1:1 (설계 §3.4).
|
|
75
|
+
const metricEnd=(reason,candidates,injected,text)=>{
|
|
76
|
+
const t=String(text||"");
|
|
77
|
+
writeLine({phase:"end",reason,candidates,injected,chars:t.length,bytes:Buffer.byteLength(t)});
|
|
78
|
+
};
|
|
74
79
|
try{
|
|
80
|
+
metricStart();
|
|
75
81
|
const p=JSON.parse(d);
|
|
76
82
|
const sid=String(p.session_id||p.sessionId||"");
|
|
77
83
|
let prompt=String(p.prompt||"").trim();
|
|
78
|
-
if(!prompt) return;
|
|
84
|
+
if(!prompt){ metricEnd("empty_prompt",0,0,""); return; } // 빈 발화 = 질의 불성립
|
|
79
85
|
prompt=prompt.slice(0,500); // RecallQuerySchema q max(500) — 초과는 400 무주입
|
|
80
86
|
const K=parseInt(process.env.ROOKIE_RECALL_PUSH_TOPK||"3",10);
|
|
81
87
|
const CAP=parseInt(process.env.ROOKIE_RECALL_PUSH_CAP||"600",10);
|
|
@@ -101,7 +107,7 @@ process.stdin.on("data",c=>d+=c).on("end",async()=>{
|
|
|
101
107
|
headers:{Authorization:"Bearer "+process.env.ROOKIE_TK,"x-rookie-hostname":process.env.ROOKIE_HOST||""},
|
|
102
108
|
signal:AbortSignal.timeout(TIMEOUT)
|
|
103
109
|
});
|
|
104
|
-
if(!res.ok) return;
|
|
110
|
+
if(!res.ok){ metricEnd("http_error",0,0,""); return; }
|
|
105
111
|
const body=await res.json();
|
|
106
112
|
const results=Array.isArray(body&&body.results)?body.results:[];
|
|
107
113
|
// 임계 없음 — 걸러내는 것은 형식 불량(id·요약 없음, 점수 비수치)과 기주입뿐이다.
|
|
@@ -109,7 +115,7 @@ process.stdin.on("data",c=>d+=c).on("end",async()=>{
|
|
|
109
115
|
.filter(r=>r&&typeof r.relevance==="number"&&r.id&&typeof r.summary==="string"&&r.summary.trim()&&!seen.has(r.id))
|
|
110
116
|
.sort((a,b)=>b.relevance-a.relevance)
|
|
111
117
|
.slice(0,K);
|
|
112
|
-
if(!cands.length){
|
|
118
|
+
if(!cands.length){ metricEnd("no_hit",results.length,0,""); return; }
|
|
113
119
|
|
|
114
120
|
// 주입 문구는 지시가 아니라 자료다. 관련도를 보증하지 않으므로 라벨에 명시하고,
|
|
115
121
|
// 날짜·출처를 앞에 놓아 ITEM 절삭에 잘려 나가지 않게 한다.
|
|
@@ -123,16 +129,20 @@ process.stdin.on("data",c=>d+=c).on("end",async()=>{
|
|
|
123
129
|
if(msg.length+item.length>CAP) break; // 총량 상한 — relevance 순 절삭
|
|
124
130
|
msg+=item; used.push(r.id);
|
|
125
131
|
}
|
|
126
|
-
if(!used.length){
|
|
132
|
+
if(!used.length){ metricEnd("no_hit",results.length,0,""); return; }
|
|
127
133
|
if(stFile){
|
|
128
134
|
try{
|
|
129
135
|
fs.mkdirSync(process.env.ROOKIE_PUSH_DIR,{recursive:true,mode:0o700});
|
|
130
136
|
fs.writeFileSync(stFile,JSON.stringify({epoch,ids:st.ids.concat(used)}),{mode:0o600});
|
|
131
137
|
}catch{} // dedup 기록 실패 → 다음 턴 중복 주입 위험만, 주입은 진행
|
|
132
138
|
}
|
|
133
|
-
|
|
139
|
+
metricEnd("ok",results.length,used.length,msg);
|
|
134
140
|
process.stdout.write(JSON.stringify({hookSpecificOutput:{hookEventName:"UserPromptSubmit",additionalContext:msg}}));
|
|
135
|
-
}catch{
|
|
141
|
+
}catch{
|
|
142
|
+
// fetch 타임아웃·네트워크·JSON 파싱 실패 전부. 세분하지 않는다(설계 §3.4) — 이 계측의 목적은
|
|
143
|
+
// 타임아웃 유실을 세는 것이지 실패 원인을 분류하는 것이 아니다.
|
|
144
|
+
try{ metricEnd("error",0,0,""); }catch{}
|
|
145
|
+
}
|
|
136
146
|
});
|
|
137
147
|
' 2>/dev/null
|
|
138
148
|
|
package/version.json
CHANGED