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
+20 -1
View File
@@ -26,6 +26,7 @@ from scribe.services import notes as notes_svc
from scribe.services import projects as projects_svc
from scribe.services import shape_ledger as shape_ledger_svc
from scribe.services import snippets as snippets_svc
from scribe.services import task_claims as task_claims_svc
from scribe.services.access import label_shared_items, owner_names_for
from scribe.services.embeddings import (
document_title,
@@ -3055,7 +3056,8 @@ def _goal_line(goal: str, project_id: int) -> str:
async def build_session_context(
user_id: int, project_id: int = 0, unbound_repo: str = ""
user_id: int, project_id: int = 0, unbound_repo: str = "",
source: str = "", session_id: str = "",
) -> dict:
"""Render the SessionStart context for a user, optionally project-scoped.
@@ -3069,6 +3071,12 @@ async def build_session_context(
unbound_repo: when the hook sent a repo remote that maps to no project,
its normalized key — triggers a one-line "bind this repo" hint so
the binding is self-healing.
source / session_id: the host's SessionStart `source` and the
session's id, when the adapter sends them. They decide what the
claim section says (milestone 381 step 3, `task_claims.
render_claims`): a compaction gets back the work it had claimed
with its latest logs; a new session hears about other sessions'
live and abandoned claims; a resume hears nothing.
Returns {"context": str, "project": dict | None}.
@@ -3135,6 +3143,17 @@ async def build_session_context(
f"`get_design_system({design['id']})` → "
f"`resolved_guidance`.",
]
# The claim section goes after the project block and before any "nothing
# loaded" note: claimed work is the most specific thing this session can be
# told, and it is true whether or not a project resolved. Best-effort — a
# session start never fails on it.
try:
lines += await task_claims_svc.claims_for_session_start(
user_id, project_dict["id"] if project_dict else 0, source, session_id,
)
except Exception: # noqa: BLE001 - context is best-effort
logger.warning("claim section skipped", exc_info=True)
# Nothing loaded — say which nothing (#4085). This used to hang off the
# `if project_id:` above as an `elif`, which meant an id that was SENT and
# did not resolve produced no message at all: the outer branch was taken,