therookie 0.4.13 → 0.4.15
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/templates/hook/bin/recall-episode-metric.sh +119 -24
- package/version.json +2 -2
package/package.json
CHANGED
|
@@ -72,6 +72,33 @@ try {
|
|
|
72
72
|
const ENVQ=/(포트|유저명|계정|경로|도메인|호스트|엔드포인트|서버\s*주소)[^\n]{0,12}(뭐|몇|어디|알아|기억|였)/;
|
|
73
73
|
const isSignal=(t)=>PAST_TIME.test(t)||PAST_HOW.test(t)||REPRO.test(t)||CONTINUE.test(t)||DECIDED.test(t)||ENVQ.test(t)||AMNESIA.test(t);
|
|
74
74
|
|
|
75
|
+
// ── v2 §D2: 교정 발화 (계측 라벨 전용) ────────────────────────────────────
|
|
76
|
+
// 두목이 "이미 아는 걸 왜 모르냐"고 짚는 발화. **강제 경로(user-prompt-skill-gate.sh)에는 넣지
|
|
77
|
+
// 않는다** — 넣는 순간 spec 2026-07-25 §7 이 금지한 "9번째 새 트리거 추가"가 된다.
|
|
78
|
+
// 집합은 오늘 사고 발화 + 오탐 후보로 실측해 확정(교정 9/10 hit, 일반 8/8 미탐지).
|
|
79
|
+
const CORRECTION=new RegExp([
|
|
80
|
+
"(저장|기록)(해두지|해놓지|한\\s*거|해\\s*둔\\s*거)?\\s*(않았|안\\s*했|안했)",
|
|
81
|
+
"기억(하고|해두고|해\\s*두고)\\s*있(잖|었잖)",
|
|
82
|
+
"(툴|도구)(에|로|에다)\\s*(저장|기록)",
|
|
83
|
+
"회상(해|해봐|해도|부터|은?\\s*했)",
|
|
84
|
+
"찾아(봤|보지|봐야)\\s*(않았|았어야|어야)",
|
|
85
|
+
"더루키(에서|에|를)[^\\n]{0,20}(찾|봐|확인|안\\s*찾|찾지)",
|
|
86
|
+
"(전에|이미|지난번에?)\\s*[^\\n]{0,10}?(했|됐|만들었|성공했)(잖|었잖)"
|
|
87
|
+
].join("|"));
|
|
88
|
+
|
|
89
|
+
// ── v2 §D1: human 프롬프트 판정 ──────────────────────────────────────────
|
|
90
|
+
// 1단 = transcript 의 isMeta 필드(Skill 로드 결과·이미지 첨부·local-command-caveat 를 정확히 거른다).
|
|
91
|
+
// 2단 = isMeta 가 안 붙는 나머지를 문자열로 배제. 판정 실패는 **포함 방향**(fail-open) —
|
|
92
|
+
// human 을 놓치는 것이 분모 결함의 재발이라 그쪽이 더 비싸다.
|
|
93
|
+
const SYS_PREFIX=/^\s*<(command-name|task-notification|system-reminder|local-command|command-message)/;
|
|
94
|
+
const isHumanPrompt=(j,txt)=>{
|
|
95
|
+
if(j && j.isMeta===true) return false;
|
|
96
|
+
if(SYS_PREFIX.test(txt)) return false;
|
|
97
|
+
if(/^\s*\[Request interrupted by user/.test(txt)) return false;
|
|
98
|
+
if(/^\s*\/\S+$/.test(txt)) return false; // 붙여넣은 파일경로 1줄
|
|
99
|
+
return true;
|
|
100
|
+
};
|
|
101
|
+
|
|
75
102
|
// ── compact/clear boundary offsets (이 transcript 것만, 정렬) ──────────────
|
|
76
103
|
let boundaries=[];
|
|
77
104
|
try {
|
|
@@ -138,22 +165,41 @@ try {
|
|
|
138
165
|
if(role==="user"){
|
|
139
166
|
const txt=userText(m);
|
|
140
167
|
if(!txt) continue; // tool_result-only user 라인 등은 프롬프트 아님
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
168
|
+
// v2 §D1 — human 프롬프트만 episode 를 연다. 시스템 주입(Skill 로드·이미지·훅 알림)은 제외.
|
|
169
|
+
if(!isHumanPrompt(j,txt)) continue;
|
|
170
|
+
const sig=isSignal(txt), amn=AMNESIA.test(txt), corr=CORRECTION.test(txt);
|
|
171
|
+
|
|
172
|
+
// v2 §D3 — 교정 발화 귀속. recall_called 전제를 제거하고(회상 안 한 쪽이 더 나쁜 신호),
|
|
173
|
+
// filler episode(도구 0건)는 건너뛰어 실제 작업 episode 에 붙인다. compact 경계는 안 넘는다.
|
|
174
|
+
if(amn||corr){
|
|
175
|
+
for(let k=episodes.length-1;k>=0;k--){
|
|
176
|
+
const e=episodes[k];
|
|
177
|
+
if(lineOff>=e._boundary) break; // 이 교정은 e 의 compact 구간 밖 — 더 거슬러 갈 수 없다
|
|
178
|
+
if(!e._sawTool) continue; // filler — 책임을 붙이지 않는다
|
|
179
|
+
if(amn) e.amnesia_next_prompt=true;
|
|
180
|
+
if(corr) e.correction_next_prompt=true;
|
|
181
|
+
break;
|
|
182
|
+
}
|
|
155
183
|
}
|
|
156
|
-
|
|
184
|
+
|
|
185
|
+
cur={
|
|
186
|
+
episode_seq: seq++,
|
|
187
|
+
episode_start_byte_offset: lineOff,
|
|
188
|
+
parser_version: 3,
|
|
189
|
+
is_human_prompt: true,
|
|
190
|
+
recall_signal: sig, recall_called: false, verified_solution_served: false,
|
|
191
|
+
external_search: false, amnesia_next_prompt: false, correction_next_prompt: false,
|
|
192
|
+
// spec 2026-08-06 A.5b — external_search 는 이제 **웹 도구만** 센다.
|
|
193
|
+
// external_search_after_recall: 회상 이후의 외부검색만(순서 있는 신호).
|
|
194
|
+
// bash_http_seen: 느슨한 Bash HTTP 관측 — 진단 전용, headline 금지.
|
|
195
|
+
external_search_after_recall: false, bash_http_seen: false,
|
|
196
|
+
first_action_after_recall: null, failure_classes: [],
|
|
197
|
+
_boundary: firstBoundaryAfter(lineOff),
|
|
198
|
+
_recallSeen: false,
|
|
199
|
+
_sawTool: false,
|
|
200
|
+
_ts: typeof j.timestamp==="string" ? j.timestamp : null,
|
|
201
|
+
};
|
|
202
|
+
episodes.push(cur);
|
|
157
203
|
continue;
|
|
158
204
|
}
|
|
159
205
|
|
|
@@ -166,17 +212,27 @@ try {
|
|
|
166
212
|
if(b.type==="text" && typeof b.text==="string" && b.text.trim()) hadText=true;
|
|
167
213
|
if(b.type==="tool_use"){
|
|
168
214
|
sawToolThisMsg=true;
|
|
215
|
+
cur._sawTool=true; // filler episode 판별용(§D3 귀속에서 건너뛸 대상)
|
|
169
216
|
const name=String(b.name||"");
|
|
170
217
|
const cmd=b.input&&typeof b.input.command==="string"?b.input.command:"";
|
|
171
218
|
let action="other";
|
|
172
219
|
if(/recall/i.test(name) || /api\/rookie\/recall/.test(cmd)) action="recall";
|
|
173
220
|
else if(/^(WebSearch|WebFetch)$/.test(name)) action="external_search";
|
|
174
|
-
// 외부 Bash HTTP(curl/wget 로 밖에서 답 찾기)도 외부검색 — recall/내부 API·로컬 dev 는 제외(codex P1·P2).
|
|
175
|
-
else if(name==="Bash" && /\b(curl|wget)\b/.test(cmd) && /https?:\/\//.test(cmd) && !/api\/rookie/.test(cmd) && !/(localhost|127\.0\.0\.1|0\.0\.0\.0)/.test(cmd)) action="external_search";
|
|
176
221
|
else if(/^(Edit|Write|NotebookEdit)$/.test(name)) action="edit";
|
|
177
222
|
else if(name==="AskUserQuestion") action="ask";
|
|
223
|
+
// spec 2026-08-06 A.5b B-1 — Bash 는 외부검색으로 세지 않는다. 명령문 전체에서 curl|wget 을
|
|
224
|
+
// 찾던 옛 판정이 heredoc 본문·경로에 그 단어가 든 **로컬 스크립트**까지 외부검색으로 셌다
|
|
225
|
+
// (§7 원문 대조: 강 3건 중 2건이 이 오탐). 정확히 세려면 셸 문법을 다시 구현해야 해서 철회했다.
|
|
226
|
+
// 느슨한 관측은 버리지 않고 **진단 전용 신호로 격리**한다.
|
|
227
|
+
const bashHttp = name==="Bash" && /\b(curl|wget)\b/.test(cmd) && /https?:\/\//.test(cmd)
|
|
228
|
+
&& !/api\/rookie/.test(cmd) && !/(localhost|127\.0\.0\.1|0\.0\.0\.0)/.test(cmd)
|
|
229
|
+
if(bashHttp) cur.bash_http_seen=true;
|
|
178
230
|
if(action==="recall"){ cur.recall_called=true; cur._recallSeen=true; }
|
|
179
|
-
if(action==="external_search"){
|
|
231
|
+
if(action==="external_search"){
|
|
232
|
+
cur.external_search=true;
|
|
233
|
+
// B-2 — 첫 행동 기록 여부와 **무관하게** 누적한다(회상 이후의 모든 외부검색).
|
|
234
|
+
if(cur._recallSeen) cur.external_search_after_recall=true;
|
|
235
|
+
}
|
|
180
236
|
// recall 이후의 첫 실질 행동을 1회만 기록.
|
|
181
237
|
if(cur._recallSeen && action!=="recall" && cur.first_action_after_recall===null){
|
|
182
238
|
cur.first_action_after_recall=action;
|
|
@@ -203,19 +259,58 @@ try {
|
|
|
203
259
|
if(e.verified_solution_served && e.first_action_after_recall==="external_search")
|
|
204
260
|
fc.push("ignored_verified_solution");
|
|
205
261
|
if(e.recall_called && e.amnesia_next_prompt) fc.push("user_correction_after_memory_available");
|
|
262
|
+
// v2 §D3 — 교정 발화를 recall 여부로 가른다. 회상조차 안 하고 지적당한 쪽이 더 나쁜 신호다.
|
|
263
|
+
if(e.correction_next_prompt) fc.push(e.recall_called?"correction_after_recall":"correction_after_no_recall");
|
|
206
264
|
e.failure_classes=fc;
|
|
207
|
-
delete e._boundary; delete e._recallSeen;
|
|
208
265
|
}
|
|
209
266
|
|
|
210
|
-
//
|
|
211
|
-
|
|
212
|
-
|
|
267
|
+
// ── v2 §D6: 활성 시각 컷오프 ─────────────────────────────────────────────
|
|
268
|
+
// 파서는 매 Stop 마다 transcript 를 처음부터 재파싱한다. 컷오프가 없으면 배포 후 살아 있는
|
|
269
|
+
// 옛 세션이 **배포 이전 발화까지** v2 행으로 적재해 "v2 = 배포 후" 전제가 깨진다.
|
|
270
|
+
// 활성 시각은 첫 v2 실행 때 한 번만 기록(wx = 원자적, 이미 있으면 유지).
|
|
271
|
+
// ⚠️ **기록에 성공한 시각만 채택한다.** 메모리상의 "지금"을 쓰면 매 Stop 마다 컷오프가
|
|
272
|
+
// 앞으로 밀려(불안정) 무엇이 왜 빠졌는지 사후 확인이 불가능해진다.
|
|
273
|
+
let activatedAt=null;
|
|
274
|
+
try{
|
|
275
|
+
const ap=path.join(bndDir,".v3-activated");
|
|
276
|
+
const readAct=()=>{ try{ const v=JSON.parse(fs.readFileSync(ap,"utf8")).activated_at; return typeof v==="string"?v:null; }catch{ return null; } };
|
|
277
|
+
activatedAt=readAct();
|
|
278
|
+
if(!activatedAt){
|
|
279
|
+
const now=new Date().toISOString();
|
|
280
|
+
try{
|
|
281
|
+
fs.mkdirSync(bndDir,{recursive:true,mode:0o700});
|
|
282
|
+
fs.writeFileSync(ap,JSON.stringify({activated_at:now}),{flag:"wx",mode:0o600});
|
|
283
|
+
activatedAt=now;
|
|
284
|
+
}catch{ activatedAt=readAct(); } // 경쟁(형제가 먼저 만듦)이면 그 값. 손상·쓰기불가면 null.
|
|
285
|
+
}
|
|
286
|
+
}catch{}
|
|
287
|
+
// timestamp 없는/파싱 불가 개시 라인은 **제외**(fail-closed). D1 의 human 판정과 방향이 반대인데
|
|
288
|
+
// 의도적이다 — 저기서는 분모 누락이, 여기서는 baseline 오염이 위험이다.
|
|
289
|
+
// 비교는 문자열이 아니라 epoch ms 로 한다(codex 코드검토 R1 P2): transcript timestamp 는
|
|
290
|
+
// ms 유무·offset 표기가 섞일 수 있어 문자열 대소 비교가 경계에서 틀린다.
|
|
291
|
+
const actMs = activatedAt ? Date.parse(activatedAt) : NaN;
|
|
292
|
+
// 활성 시각을 세우지 못하면(파일 IO·JSON 오류) 전량 emit 하지 않는다 — 컷오프 없는 emit 은
|
|
293
|
+
// 배포 이전 발화를 v2 baseline 에 섞는 것이라 계측 목적 자체를 깬다(fail-closed).
|
|
294
|
+
const emitted = Number.isFinite(actMs)
|
|
295
|
+
? episodes.filter(e=>{ const t=typeof e._ts==="string"?Date.parse(e._ts):NaN; return Number.isFinite(t) && t>=actMs; })
|
|
296
|
+
: [];
|
|
297
|
+
for(const e of emitted){ delete e._boundary; delete e._recallSeen; delete e._sawTool; delete e._ts; }
|
|
298
|
+
|
|
299
|
+
// route schema 는 max 500 — 긴 세션이 400 으로 전량 드롭되지 않게 최근 500 만 보낸다(codex P2).
|
|
300
|
+
// 버린 개수를 함께 실어 조용한 절단을 관측 가능하게 만든다(§D1).
|
|
301
|
+
const truncated = Math.max(0, emitted.length-500);
|
|
302
|
+
process.stdout.write(JSON.stringify({episodes:emitted.slice(-500), truncated_count:truncated}));
|
|
303
|
+
} catch { process.stdout.write("{\"episodes\":[],\"truncated_count\":0}"); }
|
|
213
304
|
' 2>/dev/null)
|
|
214
305
|
|
|
215
306
|
[ -z "$EPISODES" ] && exit 0
|
|
216
|
-
[ "$EPISODES" = "[]" ] && exit 0
|
|
217
307
|
|
|
218
|
-
|
|
308
|
+
# 파서는 {episodes, truncated_count} 를 낸다(v2). episode 0건이면 보낼 것이 없다.
|
|
309
|
+
BODY=$(ROOKIE_SID="$SESSION_ID" ROOKIE_EP="$EPISODES" node -e 'try{
|
|
310
|
+
const p=JSON.parse(process.env.ROOKIE_EP);
|
|
311
|
+
if(!p||!Array.isArray(p.episodes)||p.episodes.length===0) process.exit(0);
|
|
312
|
+
process.stdout.write(JSON.stringify({session_id:process.env.ROOKIE_SID,episodes:p.episodes,truncated_count:p.truncated_count||0}));
|
|
313
|
+
}catch{}' 2>/dev/null)
|
|
219
314
|
[ -z "$BODY" ] && exit 0
|
|
220
315
|
|
|
221
316
|
# 계측 POST — 세션 종료를 지연시키지 않게 background + fail-soft.
|
package/version.json
CHANGED