therookie 0.4.36 → 0.4.37

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "therookie",
3
- "version": "0.4.36",
3
+ "version": "0.4.37",
4
4
  "description": "Persistent memory for Claude Code and Codex CLI — teach your AI once and it remembers across sessions, tools, and machines. The AI new hire that remembers what you teach it.",
5
5
  "license": "MIT",
6
6
  "keywords": [
@@ -35,6 +35,9 @@ export function listPendingExtractions({ orphanRoot }) {
35
35
  // spec 2026-09-08 §2.4 — 'no_writes'(저장 0건) 와 비정상 종료를 구분해 표시한다.
36
36
  // 섞이면 진짜 복구 대기가 묻힌다.
37
37
  reason: m.reason || 'crash',
38
+ // 'stale_writes' 는 마지막 저장 시각을 같이 싣는다 — 어디부터가 미저장인지가
39
+ // 초안을 읽는 쪽의 첫 질문이다.
40
+ since: m.since || null,
38
41
  recoveredAt: m.recovered_at || null,
39
42
  // codex marker 는 codex 서브루트에 산다 — marker 명령이 이 루트를 겨냥해야
40
43
  // 큐에서 실제로 빠진다(코드검토 P2). orphan-recovery.sh 의 ROOKIE_ORPHAN_DIR 규약.
@@ -105,7 +108,12 @@ export function renderDraft(entry, draft) {
105
108
  const lines = []
106
109
  const clean = (s) => redactDraftText(s).redacted.replace(/\s+/g, ' ').trim()
107
110
  // spec 2026-09-08 §2.4 — 사유를 앞에 붙인다. 저장 0건과 비정상 종료는 봐야 할 것이 다르다.
108
- const why = entry.reason === 'no_writes' ? '저장 0건' : '비정상 종료'
111
+ const why =
112
+ entry.reason === 'no_writes'
113
+ ? '저장 0건'
114
+ : entry.reason === 'stale_writes'
115
+ ? `마지막 저장 뒤 미저장 구간${entry.since ? ` (저장 ${entry.since} 이후)` : ''}`
116
+ : '비정상 종료'
109
117
  lines.push(
110
118
  `◦ [${entry.runtime}] 세션 ${hashId8(entry.sid)} · ${why} (recovered ${entry.recoveredAt ?? 'unknown'})`
111
119
  )
@@ -35,14 +35,17 @@ export function resolveSessionId(explicit) {
35
35
  }
36
36
 
37
37
  /**
38
- * 저장이 생겼으니 "저장 0건" 대기는 더 이상 참이 아니다 — 그 표시만 지운다.
38
+ * 저장이 생겼으니 "저장 0건"·"마지막 저장 뒤 미저장 구간" 대기는 더 이상 참이 아니다 —
39
+ * 그 표시만 지운다(이어 연 세션이 그 구간을 저장한 경우).
39
40
  * 비정상 종료 복구 대기(reason 없음)는 tail 이 아직 안 건져졌으므로 건드리지 않는다.
40
41
  */
42
+ const SAVE_CLEARABLE = new Set(['no_writes', 'stale_writes'])
43
+
41
44
  export function clearNoWritesPending(sessionId) {
42
45
  if (!sessionId) return false
43
46
  try {
44
47
  const f = join(orphanRoot(), 'pending-extraction', sessionId + '.json')
45
- if (JSON.parse(readFileSync(f, 'utf8'))?.reason !== 'no_writes') return false
48
+ if (!SAVE_CLEARABLE.has(JSON.parse(readFileSync(f, 'utf8'))?.reason)) return false
46
49
  rmSync(f, { force: true })
47
50
  return true
48
51
  } catch {
@@ -305,9 +305,16 @@ if [ "$HTTP_CODE" = "200" ]; then
305
305
  # 어디에도 남지 않는다. transcript 는 있으므로 다음 세션이 건지도록 미추출 큐에 올린다.
306
306
  # 내용 없는 세션(MESSAGE_COUNT=0)은 제외 — 건질 게 없다.
307
307
  if command -v orphan_session_had_write >/dev/null 2>&1 \
308
- && [ "${MESSAGE_COUNT:-0}" -gt 0 ] 2>/dev/null \
309
- && ! orphan_session_had_write "$SESSION_ID"; then
310
- orphan_mark_no_writes "$SESSION_ID" "$TRANSCRIPT_PATH" || true
308
+ && [ "${MESSAGE_COUNT:-0}" -gt 0 ] 2>/dev/null; then
309
+ if ! orphan_session_had_write "$SESSION_ID"; then
310
+ orphan_mark_no_writes "$SESSION_ID" "$TRANSCRIPT_PATH" || true
311
+ elif command -v orphan_session_has_unsaved_tail >/dev/null 2>&1 \
312
+ && orphan_session_has_unsaved_tail "$SESSION_ID" "$TRANSCRIPT_PATH"; then
313
+ # 저장은 있었지만 그 뒤로 대화가 임계 이상 더 쌓였다 — 마지막 저장 뒤 구간은
314
+ # 아무 데도 안 남는다(2026-09-22 실측). 같은 큐에 사유만 갈라 올린다.
315
+ orphan_mark_no_writes "$SESSION_ID" "$TRANSCRIPT_PATH" "stale_writes" \
316
+ "$(orphan_session_last_write_at "$SESSION_ID")" || true
317
+ fi
311
318
  fi
312
319
  else
313
320
  # §17 H-2: 큐 적재 + 1회 chain drain (일시적 깜빡임 회복)
@@ -113,17 +113,96 @@ orphan_session_had_write() {
113
113
  [ -f "$dir/$key" ]
114
114
  }
115
115
 
116
+ # 저장 마커 경로 — had_write 와 같은 키를 쓴다(세션 구간 단위).
117
+ _orphan_session_write_marker_path() {
118
+ local sid="$1"
119
+ [ -n "$sid" ] || return 1
120
+ command -v shasum >/dev/null 2>&1 || return 1
121
+ local key
122
+ key="$(_orphan_session_write_key "$sid")"
123
+ [ -n "$key" ] || return 1
124
+ printf '%s/%s' "${ROOKIE_SESSION_WRITES_DIR:-$HOME/.rookie/session-writes}" "$key"
125
+ }
126
+
127
+ # 마지막 저장 시각(ISO UTC). 마커가 없거나 못 읽으면 빈 출력.
128
+ # date -r 은 BSD/GNU 해석이 갈려 쓰지 않는다 — node 로 mtime 을 읽는다.
129
+ orphan_session_last_write_at() {
130
+ local sid="$1" marker
131
+ marker="$(_orphan_session_write_marker_path "$sid")" || return 1
132
+ [ -f "$marker" ] || return 1
133
+ node -e "
134
+ const fs = require('node:fs');
135
+ try { console.log(new Date(fs.statSync(process.argv[1]).mtimeMs).toISOString()); }
136
+ catch { console.log(''); }
137
+ " "$marker" 2>/dev/null
138
+ }
139
+
140
+ # 마지막 저장 이후 쌓인 대화 수. 셀 수 없으면 0 — 무동작이 오탐보다 안전하다.
141
+ orphan_session_tail_count() {
142
+ local sid="$1" transcript="$2" marker
143
+ [ -n "$transcript" ] && [ -f "$transcript" ] || { printf '0'; return 0; }
144
+ marker="$(_orphan_session_write_marker_path "$sid")" || { printf '0'; return 0; }
145
+ [ -f "$marker" ] || { printf '0'; return 0; }
146
+ node -e "
147
+ const fs = require('node:fs');
148
+ const out = (n) => { console.log(String(n)); process.exit(0); };
149
+ let since;
150
+ try { since = fs.statSync(process.argv[2]).mtimeMs; } catch { out(0); }
151
+ let raw;
152
+ try { raw = fs.readFileSync(process.argv[1], 'utf8'); } catch { out(0); }
153
+ let n = 0;
154
+ for (const line of raw.split('\n')) {
155
+ const t = line.trim(); if (!t) continue;
156
+ let j; try { j = JSON.parse(t); } catch { continue; }
157
+ const m = j.message || j;
158
+ if (m.role !== 'user' && m.role !== 'assistant') continue;
159
+ const ts = Date.parse(j.timestamp || m.timestamp || '');
160
+ if (!Number.isFinite(ts) || ts <= since) continue;
161
+ let c = m.content;
162
+ if (typeof c !== 'string') {
163
+ c = Array.isArray(c)
164
+ ? c.filter(b => b && b.type === 'text' && typeof b.text === 'string').map(b => b.text).join('\n').trim()
165
+ : '';
166
+ } else { c = c.trim(); }
167
+ if (!c) continue;
168
+ n++;
169
+ }
170
+ out(n);
171
+ " "$transcript" "$marker" 2>/dev/null || printf '0'
172
+ }
173
+
174
+ # 마지막 저장 뒤로 임계 이상이 쌓였는가 — Stop hook 이 돌았다는 것과 그 구간이 기억에
175
+ # 남았다는 것은 다른 일이다(2026-09-22 실측: 세션 5개가 flushed source=stop 으로 닫히고
176
+ # 마지막 저장 뒤 30분 넘는 구간이 카드 없이 사라졌다).
177
+ #
178
+ # 기본 3 인 이유: 세는 것은 본문 있는 user·assistant 메시지라 도구 호출만 한 턴은 안 세인다.
179
+ # 같은 실측에서 두목 지시 한 건을 받아 처리하고 끝난 세션의 tail 이 3 이었고, 42·10 인 세션과
180
+ # 함께 잡혀야 했다. 저장으로 잘 닫은 세션은 뒤에 인사 한두 줄만 남아 3 에 못 미친다 —
181
+ # 임계를 낮게 잡되 0 으로 두지 않는 이유가 그것이다(큐가 넓어지면 다음 세션이 큐를 안 믿는다).
182
+ orphan_session_has_unsaved_tail() {
183
+ local sid="$1" transcript="$2" count min
184
+ min="${ROOKIE_UNSAVED_TAIL_MIN:-3}"
185
+ case "$min" in (*[!0-9]*|'') min=3 ;; esac
186
+ count="$(orphan_session_tail_count "$sid" "$transcript")"
187
+ case "$count" in (*[!0-9]*|'') return 1 ;; esac
188
+ [ "$count" -ge "$min" ]
189
+ }
190
+
116
191
  # 저장 0건이면 pending-extraction 에 reason=no_writes 로 올린다.
117
192
  # 비정상 종료(crash) 사유와 섞이지 않게 reason 을 남긴다 — 섞이면 진짜 복구 대기가 묻힌다.
118
193
  orphan_mark_no_writes() {
119
- local sid="$1" transcript="${2:-}"
194
+ local sid="$1" transcript="${2:-}" reason="${3:-no_writes}" since="${4:-}"
120
195
  [ -n "$sid" ] || return 1
196
+ # reason·since 는 marker JSON 에 그대로 박힌다 — 따옴표·백슬래시가 섞이면 읽는 쪽이 깨진다.
197
+ reason="$(printf '%s' "$reason" | tr -cd 'A-Za-z0-9_.:-')"
198
+ [ -n "$reason" ] || reason="no_writes"
199
+ since="$(printf '%s' "$since" | tr -cd 'A-Za-z0-9_.:-')"
121
200
  # 이미 추출을 마쳤거나 대기 중이면 덮지 않는다.
122
201
  [ -f "$(_orphan_client_extracted_dir)/$sid.json" ] && return 0
123
202
  [ -f "$(_orphan_pending_dir)/$sid.json" ] && return 0
124
203
  _orphan_write_marker "$(_orphan_pending_dir)" "$sid" \
125
- "$(printf '{"status":"pending_extraction","reason":"no_writes","transcript_path":"%s","recovered_at":"%s"}' \
126
- "$transcript" "$(date -u +%Y-%m-%dT%H:%M:%SZ)")"
204
+ "$(printf '{"status":"pending_extraction","reason":"%s","since":"%s","transcript_path":"%s","recovered_at":"%s"}' \
205
+ "$reason" "$since" "$transcript" "$(date -u +%Y-%m-%dT%H:%M:%SZ)")"
127
206
  }
128
207
 
129
208
  # P0.4 — 짧고 내용 없는(low_value) 세션 marker. record_pending 대신 호출 → 재적재 차단.
package/version.json CHANGED
@@ -1,4 +1,4 @@
1
1
  {
2
- "cli_version": "0.4.36",
2
+ "cli_version": "0.4.37",
3
3
  "skill_version": "1.40.1"
4
4
  }