feat(tasks): SessionStart reads the claim — a compaction gets its work back (milestone 381 step 3)
CI & Build / Python lint (push) Successful in 2s
CI & Build / Plugin hooks (push) Successful in 9s
CI & Build / integration (push) Successful in 50s
CI & Build / TypeScript typecheck (push) Successful in 52s
CI & Build / Python tests (push) Successful in 1m35s
CI & Build / Build & push image (push) Canceled after 45s

The claim now pays rent to the session that set it. The SessionStart hook
sends the host's `source` and the session id; the server renders a claim
section by source (task_claims.render_claims):

- compact / clear: the work this session had claimed, each with its two
  latest log entries, so a compacted session resumes from the record
  rather than from a count of open tasks.
- startup / clear: other sessions' live claims (may still be running) and
  in-progress tasks whose claim went quiet (abandoned mid-task).
- fork: the same, framed as "two sessions may now hold this".
- resume, or no source sent: nothing.

The task sidebar shows a live claim as "Being worked" and a dead one on
open work as "Went quiet", so the operator sees a session die mid-task.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-24 06:41:19 -04:00
co-authored by Claude Opus 5.5
parent a585fe0be0
commit e46eea3b52
8 changed files with 313 additions and 7 deletions
+88
View File
@@ -168,3 +168,91 @@ async def test_a_closed_task_binds_nothing(users):
task = await notes_svc.create_note(owner, title="claim closed", status="in_progress")
await notes_svc.update_note(owner, task.id, status="done")
assert await tc.bind_session(owner, task.id, "sess") is None
# --- step 3: the readers ----------------------------------------------------
from types import SimpleNamespace # noqa: E402
_NOW = datetime(2026, 9, 24, 12, 0, tzinfo=timezone.utc)
def _claimed(id, session, touched_ago, status="in_progress", title=None):
t = _NOW - touched_ago
return SimpleNamespace(
id=id, title=title or f"task {id}", status=status,
claimed_by=1, claimed_at=t, claim_touched_at=t, claim_session=session,
)
def _log(content, ago=timedelta(minutes=5)):
return SimpleNamespace(content=content, created_at=_NOW - ago)
def _render(source, claims, logs=None, sid="me"):
return "\n".join(tc.render_claims(source, sid, claims, logs or {}, now=_NOW))
def test_a_compaction_gets_back_its_own_claimed_work_and_latest_logs():
"""The measurement the milestone names: a compacted session comes back
holding its own state, without being told to go looking."""
mine = _claimed(10, "me", timedelta(minutes=3), title="wire the reader")
out = _render("compact", [mine], {10: [_log("ruled out the cache theory")]})
assert "#10" in out and "wire the reader" in out
assert "ruled out the cache theory" in out
def test_a_resume_says_nothing():
mine = _claimed(10, "me", timedelta(minutes=3))
assert _render("resume", [mine]) == ""
def test_a_startup_names_other_sessions_live_and_abandoned_claims():
live = _claimed(11, "other", timedelta(minutes=10))
gone = _claimed(12, "older", timedelta(days=3))
out = _render("startup", [live, gone])
assert "#11" in out and "may still be running" in out
assert "#12" in out and "went quiet" in out
def test_a_startup_does_not_push_this_sessions_own_work():
"""A new session id owns nothing yet; the own-work push is for a context
that was lost, not one that never existed."""
mine = _claimed(10, "me", timedelta(minutes=3))
assert "In flight" not in _render("startup", [mine])
def test_a_fork_is_told_two_sessions_may_hold_the_same_claim():
parent = _claimed(13, "parent", timedelta(minutes=2))
out = _render("fork", [parent], sid="child")
assert "forked" in out and "#13" in out
def test_a_dead_claim_on_finished_work_is_not_news():
done = _claimed(14, "older", timedelta(days=3), status="done")
assert _render("startup", [done]) == ""
def test_the_session_start_hook_sends_the_source_and_the_session():
"""The reader branches on what the hook sends; a hook that stopped sending
either would leave every session on the no-claims path, silently."""
text = (ROOT / "plugin/hooks/scribe_session_context.sh").read_text()
assert re.search(r'q="source=\$\(printf', text)
assert "session_id=$(printf" in text
@pytest.mark.integration
async def test_session_start_after_a_compaction_carries_the_claimed_task(users):
from scribe.services import task_logs
from scribe.services.plugin_context import build_session_context
owner, _ = users
task = await notes_svc.create_note(owner, title="claimed then compacted",
status="in_progress")
await task_logs.create_log(owner, task.id, "halfway: the migration is written")
await tc.bind_session(owner, task.id, "sess-compact")
ctx = (await build_session_context(
owner, source="compact", session_id="sess-compact"))["context"]
assert "claimed then compacted" in ctx
assert "the migration is written" in ctx