therookie 0.4.13 → 0.4.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/templates/hook/bin/recall-episode-metric.sh +103 -21
- 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,37 @@ 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: 2,
|
|
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
|
+
first_action_after_recall: null, failure_classes: [],
|
|
193
|
+
_boundary: firstBoundaryAfter(lineOff),
|
|
194
|
+
_recallSeen: false,
|
|
195
|
+
_sawTool: false,
|
|
196
|
+
_ts: typeof j.timestamp==="string" ? j.timestamp : null,
|
|
197
|
+
};
|
|
198
|
+
episodes.push(cur);
|
|
157
199
|
continue;
|
|
158
200
|
}
|
|
159
201
|
|
|
@@ -166,6 +208,7 @@ try {
|
|
|
166
208
|
if(b.type==="text" && typeof b.text==="string" && b.text.trim()) hadText=true;
|
|
167
209
|
if(b.type==="tool_use"){
|
|
168
210
|
sawToolThisMsg=true;
|
|
211
|
+
cur._sawTool=true; // filler episode 판별용(§D3 귀속에서 건너뛸 대상)
|
|
169
212
|
const name=String(b.name||"");
|
|
170
213
|
const cmd=b.input&&typeof b.input.command==="string"?b.input.command:"";
|
|
171
214
|
let action="other";
|
|
@@ -203,19 +246,58 @@ try {
|
|
|
203
246
|
if(e.verified_solution_served && e.first_action_after_recall==="external_search")
|
|
204
247
|
fc.push("ignored_verified_solution");
|
|
205
248
|
if(e.recall_called && e.amnesia_next_prompt) fc.push("user_correction_after_memory_available");
|
|
249
|
+
// v2 §D3 — 교정 발화를 recall 여부로 가른다. 회상조차 안 하고 지적당한 쪽이 더 나쁜 신호다.
|
|
250
|
+
if(e.correction_next_prompt) fc.push(e.recall_called?"correction_after_recall":"correction_after_no_recall");
|
|
206
251
|
e.failure_classes=fc;
|
|
207
|
-
delete e._boundary; delete e._recallSeen;
|
|
208
252
|
}
|
|
209
253
|
|
|
210
|
-
//
|
|
211
|
-
|
|
212
|
-
|
|
254
|
+
// ── v2 §D6: 활성 시각 컷오프 ─────────────────────────────────────────────
|
|
255
|
+
// 파서는 매 Stop 마다 transcript 를 처음부터 재파싱한다. 컷오프가 없으면 배포 후 살아 있는
|
|
256
|
+
// 옛 세션이 **배포 이전 발화까지** v2 행으로 적재해 "v2 = 배포 후" 전제가 깨진다.
|
|
257
|
+
// 활성 시각은 첫 v2 실행 때 한 번만 기록(wx = 원자적, 이미 있으면 유지).
|
|
258
|
+
// ⚠️ **기록에 성공한 시각만 채택한다.** 메모리상의 "지금"을 쓰면 매 Stop 마다 컷오프가
|
|
259
|
+
// 앞으로 밀려(불안정) 무엇이 왜 빠졌는지 사후 확인이 불가능해진다.
|
|
260
|
+
let activatedAt=null;
|
|
261
|
+
try{
|
|
262
|
+
const ap=path.join(bndDir,".v2-activated");
|
|
263
|
+
const readAct=()=>{ try{ const v=JSON.parse(fs.readFileSync(ap,"utf8")).activated_at; return typeof v==="string"?v:null; }catch{ return null; } };
|
|
264
|
+
activatedAt=readAct();
|
|
265
|
+
if(!activatedAt){
|
|
266
|
+
const now=new Date().toISOString();
|
|
267
|
+
try{
|
|
268
|
+
fs.mkdirSync(bndDir,{recursive:true,mode:0o700});
|
|
269
|
+
fs.writeFileSync(ap,JSON.stringify({activated_at:now}),{flag:"wx",mode:0o600});
|
|
270
|
+
activatedAt=now;
|
|
271
|
+
}catch{ activatedAt=readAct(); } // 경쟁(형제가 먼저 만듦)이면 그 값. 손상·쓰기불가면 null.
|
|
272
|
+
}
|
|
273
|
+
}catch{}
|
|
274
|
+
// timestamp 없는/파싱 불가 개시 라인은 **제외**(fail-closed). D1 의 human 판정과 방향이 반대인데
|
|
275
|
+
// 의도적이다 — 저기서는 분모 누락이, 여기서는 baseline 오염이 위험이다.
|
|
276
|
+
// 비교는 문자열이 아니라 epoch ms 로 한다(codex 코드검토 R1 P2): transcript timestamp 는
|
|
277
|
+
// ms 유무·offset 표기가 섞일 수 있어 문자열 대소 비교가 경계에서 틀린다.
|
|
278
|
+
const actMs = activatedAt ? Date.parse(activatedAt) : NaN;
|
|
279
|
+
// 활성 시각을 세우지 못하면(파일 IO·JSON 오류) 전량 emit 하지 않는다 — 컷오프 없는 emit 은
|
|
280
|
+
// 배포 이전 발화를 v2 baseline 에 섞는 것이라 계측 목적 자체를 깬다(fail-closed).
|
|
281
|
+
const emitted = Number.isFinite(actMs)
|
|
282
|
+
? episodes.filter(e=>{ const t=typeof e._ts==="string"?Date.parse(e._ts):NaN; return Number.isFinite(t) && t>=actMs; })
|
|
283
|
+
: [];
|
|
284
|
+
for(const e of emitted){ delete e._boundary; delete e._recallSeen; delete e._sawTool; delete e._ts; }
|
|
285
|
+
|
|
286
|
+
// route schema 는 max 500 — 긴 세션이 400 으로 전량 드롭되지 않게 최근 500 만 보낸다(codex P2).
|
|
287
|
+
// 버린 개수를 함께 실어 조용한 절단을 관측 가능하게 만든다(§D1).
|
|
288
|
+
const truncated = Math.max(0, emitted.length-500);
|
|
289
|
+
process.stdout.write(JSON.stringify({episodes:emitted.slice(-500), truncated_count:truncated}));
|
|
290
|
+
} catch { process.stdout.write("{\"episodes\":[],\"truncated_count\":0}"); }
|
|
213
291
|
' 2>/dev/null)
|
|
214
292
|
|
|
215
293
|
[ -z "$EPISODES" ] && exit 0
|
|
216
|
-
[ "$EPISODES" = "[]" ] && exit 0
|
|
217
294
|
|
|
218
|
-
|
|
295
|
+
# 파서는 {episodes, truncated_count} 를 낸다(v2). episode 0건이면 보낼 것이 없다.
|
|
296
|
+
BODY=$(ROOKIE_SID="$SESSION_ID" ROOKIE_EP="$EPISODES" node -e 'try{
|
|
297
|
+
const p=JSON.parse(process.env.ROOKIE_EP);
|
|
298
|
+
if(!p||!Array.isArray(p.episodes)||p.episodes.length===0) process.exit(0);
|
|
299
|
+
process.stdout.write(JSON.stringify({session_id:process.env.ROOKIE_SID,episodes:p.episodes,truncated_count:p.truncated_count||0}));
|
|
300
|
+
}catch{}' 2>/dev/null)
|
|
219
301
|
[ -z "$BODY" ] && exit 0
|
|
220
302
|
|
|
221
303
|
# 계측 POST — 세션 종료를 지연시키지 않게 background + fail-soft.
|
package/version.json
CHANGED