os-me-extract marks a live session read, so its later moments are never extracted #1257

Closed
opened 2026-10-23 16:42:23 +00:00 by jared · 2 comments
Owner

Problem

os-me-extract marks a session read as soon as it extracts it, even when the session is still live. A live transcript grows after the run. The cursor never lets a later run read that session again, so the moments added after the run are lost. A live transcript also moves to another project folder when the session enters or leaves a worktree, and the run then fails on the old path.

Observed

On 2026-10-23 the extraction ran from session 4f3f95ea while that session was live. The session entered the worktree ticket-1245 during the run. The transcript moved from -home-jared-dev-cc-os--claude-worktrees-ticket-1256/ to -home-jared-dev-cc-os--claude-worktrees-ticket-1245/.

ruby ~/.claude/plugins/os-me/bin/os-me-extract
/home/jared/dev/cc-os/plugins/os-me/lib/os_me/transcript_reader.rb:12:in 'IO.foreach': No such file or directory @ rb_sysopen - /home/jared/.claude/projects/-home-jared-dev-cc-os/4f3f95ea-3c6f-4d23-a0ea-e349d6ab02a5.jsonl (Errno::ENOENT)

The rerun read the session at its new path and marked it read in queue/extract-cursor.json. The session continued after that, and its later memos will not be extracted.

Reproduce

n/a: needs a live session that changes worktree during an extraction run; the steps are in Observed.

Approach

Follow the cursor design in docs/decision-system/extraction.md ("A session that is still open changes mtime, so it is processed again next time"). ExtractCursor stores {session_id => transcript mtime}. A session is unread when its transcript mtime is later than the stored value. MomentExtractor#run advances the cursor for one session after that session is appended. A moment id that MomentLog already holds is not sent to ReplyShape again, so a changed session costs model calls only for its new memos. A cursor file in the old format (a JSON array of session ids) counts each listed session as seen at its transcript mtime at load time. A transcript path that no longer exists is skipped and not marked.

Required work:

  1. Key the cursor on transcript mtime — check: present File.mtime in plugins/os-me/lib/os_me/extract_cursor.rb

Acceptance criteria

  1. Given a session extracted once, when its transcript gets a new memo and reply and the extraction runs again, then the log holds the new moment and no duplicate of the old one.
  2. Given a session extracted once whose transcript did not change, when the extraction runs again, then no model call is made for it.
  3. Given a changed session with one old memo and one new memo, when the extraction runs again, then exactly one model call is made for it.
  4. Given a cursor file in the old array format that lists a session, when the extraction runs and that transcript did not change, then no model call is made for it.
  5. Given a catalog path whose file was removed before the run reads it, when the extraction runs, then the run finishes, extracts the other sessions, and does not mark the removed session.

Owner sketch

class OsMe::ExtractCursor
  def unread(paths); end
  def advance(path); end
end

Origin

  • Trigger: os-me-extract rerun for #1244 (S2c) on 2026-10-23
  • Improvised this session: reran the extraction; the per-session cursor resumed after the failure
  • Chain: lost later moments of a live session ← cursor marks a live session read (plugins/os-me/lib/os_me/moment_extractor.rb, run calls mark_read per session) ← no rule skips a session whose transcript is still being written. Stop at DESIGN (docs/decision-system/extraction.md, cursor).
  • Root candidate: none found; searched ExtractCursor, mtime, cursor, live session, incremental, extract, S6
  • Where: plugins/os-me/lib/os_me/moment_extractor.rb; plugins/os-me/lib/os_me/extract_cursor.rb
  • Session: 4f3f95ea-3c6f-4d23-a0ea-e349d6ab02a5
  • Transcript: /home/jared/.claude/projects/-home-jared-dev-cc-os/4f3f95ea-3c6f-4d23-a0ea-e349d6ab02a5.jsonl

VERDICT: CREATE

## Problem `os-me-extract` marks a session read as soon as it extracts it, even when the session is still live. A live transcript grows after the run. The cursor never lets a later run read that session again, so the moments added after the run are lost. A live transcript also moves to another project folder when the session enters or leaves a worktree, and the run then fails on the old path. ## Observed On 2026-10-23 the extraction ran from session 4f3f95ea while that session was live. The session entered the worktree `ticket-1245` during the run. The transcript moved from `-home-jared-dev-cc-os--claude-worktrees-ticket-1256/` to `-home-jared-dev-cc-os--claude-worktrees-ticket-1245/`. ``` ruby ~/.claude/plugins/os-me/bin/os-me-extract ``` ``` /home/jared/dev/cc-os/plugins/os-me/lib/os_me/transcript_reader.rb:12:in 'IO.foreach': No such file or directory @ rb_sysopen - /home/jared/.claude/projects/-home-jared-dev-cc-os/4f3f95ea-3c6f-4d23-a0ea-e349d6ab02a5.jsonl (Errno::ENOENT) ``` The rerun read the session at its new path and marked it read in `queue/extract-cursor.json`. The session continued after that, and its later memos will not be extracted. ## Reproduce n/a: needs a live session that changes worktree during an extraction run; the steps are in Observed. ## Approach Follow the cursor design in `docs/decision-system/extraction.md` ("A session that is still open changes mtime, so it is processed again next time"). `ExtractCursor` stores `{session_id => transcript mtime}`. A session is unread when its transcript mtime is later than the stored value. `MomentExtractor#run` advances the cursor for one session after that session is appended. A moment id that `MomentLog` already holds is not sent to `ReplyShape` again, so a changed session costs model calls only for its new memos. A cursor file in the old format (a JSON array of session ids) counts each listed session as seen at its transcript mtime at load time. A transcript path that no longer exists is skipped and not marked. Required work: 1. Key the cursor on transcript mtime — check: present `File.mtime` in `plugins/os-me/lib/os_me/extract_cursor.rb` ## Acceptance criteria 1. Given a session extracted once, when its transcript gets a new memo and reply and the extraction runs again, then the log holds the new moment and no duplicate of the old one. 2. Given a session extracted once whose transcript did not change, when the extraction runs again, then no model call is made for it. 3. Given a changed session with one old memo and one new memo, when the extraction runs again, then exactly one model call is made for it. 4. Given a cursor file in the old array format that lists a session, when the extraction runs and that transcript did not change, then no model call is made for it. 5. Given a catalog path whose file was removed before the run reads it, when the extraction runs, then the run finishes, extracts the other sessions, and does not mark the removed session. ## Owner sketch ```ruby class OsMe::ExtractCursor def unread(paths); end def advance(path); end end ``` ## Origin - Trigger: `os-me-extract` rerun for #1244 (S2c) on 2026-10-23 - Improvised this session: reran the extraction; the per-session cursor resumed after the failure - Chain: lost later moments of a live session ← cursor marks a live session read (`plugins/os-me/lib/os_me/moment_extractor.rb`, `run` calls `mark_read` per session) ← no rule skips a session whose transcript is still being written. Stop at `DESIGN` (`docs/decision-system/extraction.md`, cursor). - Root candidate: none found; searched ExtractCursor, mtime, cursor, live session, incremental, extract, S6 - Where: plugins/os-me/lib/os_me/moment_extractor.rb; plugins/os-me/lib/os_me/extract_cursor.rb - Session: 4f3f95ea-3c6f-4d23-a0ea-e349d6ab02a5 - Transcript: /home/jared/.claude/projects/-home-jared-dev-cc-os/4f3f95ea-3c6f-4d23-a0ea-e349d6ab02a5.jsonl VERDICT: CREATE
Author
Owner

Work started on branch ticket-1257 (os-sdlc implement pipeline).

Work started on branch ticket-1257 (os-sdlc implement pipeline).
Author
Owner

Resolution

Done: ExtractCursor stores {session_id => transcript mtime}, returns unread paths, advances one session after its moments are appended, migrates the old array cursor at load, and skips a vanished path without marking it. MomentExtractor sends only memos whose moment id the log does not hold to ReplyShape. DecisionMoment owns the id rule and TranscriptCatalog owns session_id(path).

Evidence: Implementation 126 complete (18 dispatches, 15 min 12 s, one reroute after escalation 66 for two test defects fixed by hand). Commits on ticket-1257; os-me suite 177 runs, 0 failures.

Follow-ups: none

## Resolution **Done:** ExtractCursor stores {session_id => transcript mtime}, returns unread paths, advances one session after its moments are appended, migrates the old array cursor at load, and skips a vanished path without marking it. MomentExtractor sends only memos whose moment id the log does not hold to ReplyShape. DecisionMoment owns the id rule and TranscriptCatalog owns session_id(path). **Evidence:** Implementation 126 complete (18 dispatches, 15 min 12 s, one reroute after escalation 66 for two test defects fixed by hand). Commits on ticket-1257; os-me suite 177 runs, 0 failures. **Follow-ups:** none
jared closed this issue 2026-10-23 16:58:19 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
jared/cc-os#1257
No description provided.