feat(tasks): the hand-off — SessionEnd releases a session's claims; the practice is written down (milestone 381 step 4)
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 12s
CI & Build / TypeScript typecheck (push) Successful in 52s
CI & Build / integration (push) Successful in 59s
CI & Build / Python tests (push) Successful in 1m40s
CI & Build / Build & push image (push) Successful in 24s
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 12s
CI & Build / TypeScript typecheck (push) Successful in 52s
CI & Build / integration (push) Successful in 59s
CI & Build / Python tests (push) Successful in 1m40s
CI & Build / Build & push image (push) Successful in 24s
Release is the mechanical half: scribe_session_end.sh sends the ending
session's id to /api/plugin/release-session, which releases the claims it
held. A tidy-up, not the guarantee (no SessionEnd on a crash; the lease
covers that), and skipped on /clear so SessionStart(clear) can still
hand the claimed work back.
Saying what happened is the half only the model can do. It is stated as a
practice where it is read: the using-scribe skill owns it ("Hand off before
this session's context stops existing", pinned in test_guidance_ownership),
the static context points at it for the wrap-up moment, and add_task_log's
docstring says a log claims the task. _INSTRUCTIONS is untouched.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -483,6 +483,11 @@ async def add_task_log(task_id: int, content: str) -> dict:
|
||||
cannot get from the body or the diff is what you tried, what you ruled
|
||||
out, and where it actually stands.
|
||||
|
||||
A log on an open task also marks it as being worked by this session — its
|
||||
`claim` (milestone 381). That is what brings the task and its newest
|
||||
entries back to you after a compaction; it lapses by itself once the
|
||||
session stops writing, and a status of done, cancelled or todo clears it.
|
||||
|
||||
The response shows the task's `systems` — or, if the task is an untagged
|
||||
project record, the `systems_hint` question: logging work IS working in
|
||||
some area, so answer it (update_task with system_ids, or create_system
|
||||
|
||||
@@ -388,6 +388,25 @@ async def claim_session():
|
||||
return jsonify({"claim": claim})
|
||||
|
||||
|
||||
@plugin_bp.get("/release-session")
|
||||
@login_required
|
||||
async def release_session():
|
||||
"""Release the claims a session held, as it ends (milestone 381 step 4).
|
||||
|
||||
Called by `scribe_session_end.sh`. Best-effort by design: SessionEnd does
|
||||
not fire on a crash, so the claim's lease — not this call — is what makes a
|
||||
dead session's claim read as dead. This only makes the common case tidy.
|
||||
|
||||
Query:
|
||||
session_id (str) — the ending session's id, from the hook event.
|
||||
"""
|
||||
session_id = (request.args.get("session_id") or "").strip()
|
||||
if not session_id:
|
||||
return jsonify({"error": "session_id is required"}), 400
|
||||
released = await task_claims_svc.release_session(g.user.id, session_id)
|
||||
return jsonify({"released": released})
|
||||
|
||||
|
||||
@plugin_bp.get("/processes")
|
||||
@login_required
|
||||
async def process_manifest():
|
||||
|
||||
@@ -281,3 +281,33 @@ async def claims_for_session_start(
|
||||
if len(bucket) < _LOGS_PER_TASK:
|
||||
bucket.append(row)
|
||||
return render_claims(source, session_id, claims, logs)
|
||||
|
||||
|
||||
async def release_session(user_id: int, session_id: str) -> int:
|
||||
"""Release every claim the caller holds under `session_id` (milestone 381 step 4).
|
||||
|
||||
Called by the plugin's SessionEnd hook: the session's context is about to
|
||||
stop existing, so the attention its claims record is ending too. A TIDY-UP,
|
||||
not the guarantee — SessionEnd does not fire on a crash, a killed terminal
|
||||
or a dropped connection, and those are what the lease is for. Returns how
|
||||
many were released; 0 is the ordinary answer for a session that claimed
|
||||
nothing.
|
||||
"""
|
||||
from sqlalchemy import select
|
||||
|
||||
from scribe.models import async_session
|
||||
from scribe.models.note import Note
|
||||
|
||||
session_id = (session_id or "").strip()[:200]
|
||||
if not session_id:
|
||||
return 0
|
||||
async with async_session() as session:
|
||||
held = (await session.execute(
|
||||
select(Note).where(
|
||||
Note.claimed_by == user_id, Note.claim_session == session_id,
|
||||
)
|
||||
)).scalars().all()
|
||||
for note in held:
|
||||
release_claim(note)
|
||||
await session.commit()
|
||||
return len(held)
|
||||
|
||||
Reference in New Issue
Block a user