diff --git a/hooks.lock b/hooks.lock index fdb788c..0f633c0 100644 --- a/hooks.lock +++ b/hooks.lock @@ -6,6 +6,6 @@ # no hooks.json here and nothing generates one; tools/generate.py refuses this host by # name for exactly that reason. repo=memvara/memvara -sha=eb25ea028e9f70372d2da7903215636a40df320a +sha=8f7545ead4814e2a81c1c6a05d0391cb65343443 path=plugin/hooks host=opencode diff --git a/hooks/lib/standing.py b/hooks/lib/standing.py index 443a8dc..0f550bc 100644 --- a/hooks/lib/standing.py +++ b/hooks/lib/standing.py @@ -59,6 +59,14 @@ #: The server's word for "a machine derived this". Matched as a bracket field. _INFERRED = "inferred" +#: What `memory_standing` is asked for on a hosted deployment, instead of the tool's own +#: default of 64. The deployment truncates BEFORE this module orders or filters anything, +#: and a store measured at 169 standing claims was handing back 64 of them chosen by the +#: server. 200 is the most memvara-cloud's `GET /v1/standing` accepts (`le=200` on its +#: `limit`); a larger number is refused there, not clamped, so this is a ceiling and not +#: a wish. Only the hosted route reads it: the local route holds the whole set already. +HOSTED_STANDING_MAX = 200 + #: What a marked row ends with. The library's own spelling, so a reader who has seen one #: block has seen both. The extractor's NAME is deliberately never rendered here or #: upstream: it is caller-supplied through `memory_remember`, so printing it would put @@ -137,7 +145,10 @@ def _mine(subject: str, cwd: str) -> bool: """ if subject == "user": return True - return bool(cwd) and subject == f"project:{cwd}" + # The namespace folds case, the way every typed entity does in the library + # (`Claim.subject_type`); the path does not, because paths do not. + namespace, sep, path = subject.partition(":") + return bool(cwd) and bool(sep) and namespace.lower() == "project" and path == cwd def _machine_wrote(claim: Any) -> bool: @@ -251,7 +262,7 @@ def _from_tool(store: Any) -> "list[Note] | None": call = getattr(store, "_call", None) if not callable(accepts) or not callable(call) or not accepts("memory_standing", "k"): return None - return _rows(str(call("memory_standing", {}) or "")) + return _rows(str(call("memory_standing", {"k": HOSTED_STANDING_MAX}) or "")) def _from_since(store: Any) -> "list[Note] | None": @@ -269,7 +280,14 @@ def _from_since(store: Any) -> "list[Note] | None": def _order(notes: "list[Note]") -> "list[Note]": - """Most-trusted first, then newest, then by id so the order is total. + """Stated first, then most-trusted, then newest, then by id so the order is total. + + Stated before trusted, because confidence is written by whoever wrote the claim and + a model writes its own. Measured on the real store: one extractor filed every + paraphrase it derived at 0.84 to 1.00 and the capture hook filed the user's own + sentence at 0.70, so seven machine restatements of one rule sat above the sentence + the user typed, and the budget cut off below them. The server's `memory_standing` + sorts the same way; this is the same rule on the route that parses rows. The id tiebreak is not decoration. Without a total order two claims written in the same instant swap places between runs, and a block that differs run to run is a block whose @@ -279,9 +297,11 @@ def _order(notes: "list[Note]") -> "list[Note]": stand-in value would sort real claims against a number nobody measured. """ if any(note.confidence is None for note in notes): - return list(notes) - return sorted(notes, key=lambda n: (-(n.confidence or 0.0), _negated(n.recorded), - n.ident)) + # Stated before derived is still knowable without a number: the marker is on + # the row. Stable, so within each half the server's own order stands. + return sorted(notes, key=lambda n: n.inferred) + return sorted(notes, key=lambda n: (n.inferred, -(n.confidence or 0.0), + _negated(n.recorded), n.ident)) def _negated(stamp: str) -> str: