feat(moments): the reply moment holds a finished reply for one read (milestone 458 step 4b, #4922)
CI & Build / Python lint (push) Successful in 2s
CI & Build / Plugin hooks (push) Successful in 14s
CI & Build / TypeScript typecheck (push) Successful in 55s
CI & Build / integration (push) Successful in 1m1s
CI & Build / Python tests (push) Failing after 1m25s
CI & Build / Build & push image (push) Skipped
CI & Build / Python lint (push) Successful in 2s
CI & Build / Plugin hooks (push) Successful in 14s
CI & Build / TypeScript typecheck (push) Successful in 55s
CI & Build / integration (push) Successful in 1m1s
CI & Build / Python tests (push) Failing after 1m25s
CI & Build / Build & push image (push) Skipped
The reply is the one act no tool call marks, and it is where "let me know if it works" gets said. A new Stop hook (scribe_reply_check.sh) sends the finished reply to POST /api/plugin/reply-rules, which checks it twice: - mounted: every unopened RULE on reply.report, plus reply.ask when the reply asks a question. Deterministic. - semantic: the reply's head and tail against every rule's trigger, on a new ranked surface, reply_rule. It is the backstop for whatever the earlier arms missed. Its floor is its stop bar (default 0.80, budget 1), with its own Settings dials. The new stop_only stage records surfacing for the rule that holds and nothing else, because nothing else reached anyone. Following the operator's ruling from 456 step 8, a rule that holds blocks once, in the server's words. The hook blocks only on a reason it was given, so an unreachable instance never stops a session, and it never holds the rewrite. The ledger is the act checkpoint's own, so a rule holds a session once across both doors and the per-session cap counts both. The turn reader moved from the report check into scribe_defs.sh (scribe_turn_facts / scribe_turn_fact), so the two Stop hooks read a turn the same way. The output was checked identical on a real transcript. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -294,6 +294,42 @@ async def moment():
|
||||
})
|
||||
|
||||
|
||||
@plugin_bp.post("/reply-rules")
|
||||
@login_required
|
||||
@memoized_query_embeddings
|
||||
async def reply_rules():
|
||||
"""Whether the reply that ends a turn is held for one read (milestone 458).
|
||||
|
||||
Called by the plugin's Stop hook with the finished reply. Two halves: the
|
||||
rules MOUNTED on the reply moments (`reply.report`, and `reply.ask` when
|
||||
the reply asks something), and the reply text against every rule's
|
||||
trigger — the backstop for whatever the earlier arms missed.
|
||||
|
||||
Body: `{"reply": "<text>"}`. Query: `repo` / `project_id` for the scope;
|
||||
`held_rule_ids` (opened this session — exempt, as at the act checkpoint);
|
||||
`exclude_rule_ids` (named this session — not re-counted as surfaced);
|
||||
`stopped_rule_ids` (already held a reply or an act this session — a rule
|
||||
holds once, and the per-session cap counts these).
|
||||
|
||||
Returns `reason` — the words the hook blocks with, empty when nothing
|
||||
holds — plus `rule_ids` for the hook's ledger and `moments` reached.
|
||||
"""
|
||||
data = await request.get_json(silent=True) or {}
|
||||
reply = str(data.get("reply") or "")
|
||||
project_id, _repo, _unbound = await _project_scope()
|
||||
held = await moment_delivery_svc.reply_hold(
|
||||
g.user.id, reply, project_id=project_id,
|
||||
exclude=frozenset(_int_list(request.args.get("exclude_rule_ids"))),
|
||||
held=frozenset(_int_list(request.args.get("held_rule_ids"))),
|
||||
stopped=frozenset(_int_list(request.args.get("stopped_rule_ids"))),
|
||||
)
|
||||
return jsonify({
|
||||
"reason": held.get("reason", ""),
|
||||
"rule_ids": held.get("rule_ids", []),
|
||||
"moments": held.get("moments", []),
|
||||
})
|
||||
|
||||
|
||||
@plugin_bp.get("/prior-art")
|
||||
@login_required
|
||||
@memoized_query_embeddings
|
||||
|
||||
Reference in New Issue
Block a user