Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions .beads/issues.jsonl

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions docs/cli-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,10 @@ Options:
Sort by field
--reverse Reverse sort order
--sample INTEGER Random sample of N sessions
--root / --no-root Only top-level sessions (--root) or only
subagent/branch children (--no-root). Unset
(default) selects both, unfiltered by
structure.
-o, --output TEXT Output destinations: browser, clipboard,
stdout (comma-separated)
--json Shortcut for --format json. Disables color
Expand Down
2 changes: 1 addition & 1 deletion docs/search.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ Invalid examples found in shipped teaching surfaces at the snapshot boundary:
|---|---|---|---|
| `polylogue/mcp/server_prompts.py:509` | `actions where session.repo:example-repo since:7d AND output:failed` | invalid query expression near column 27 | `actions where session.repo:example-repo AND session.since:7d AND output:failed` |
| `polylogue/mcp/server_prompts.py:524` | `files where repo:example-repo AND path:src/mcp/server.py` | field 'repo' is not supported for file predicates | `files where session.repo:example-repo AND path:src/mcp/server.py` |
| `docs/search.md:924` | `text:css {session_id example}: refactor` | unknown query field 'text'; recognized fields: action, assistant_messages, assistant_words, authored_user_messages, authored_user_words, contains, cwd, duration_ms, has, id, lane, lineage, messages, near, origin, paste_messages, path, project, repo, session, since, system_messages, tag, thinking_messages, title, tool, tool_messages, tool_use_messages, until, user_messages, user_words, words | `contains:"css refactor"` |
| `docs/search.md:924` | `text:css {session_id example}: refactor` | unknown query field 'text'; recognized fields: action, assistant_messages, assistant_words, authored_user_messages, authored_user_words, contains, cwd, duration_ms, has, id, lane, lineage, messages, near, origin, paste_messages, path, project, repo, root, session, since, system_messages, tag, thinking_messages, title, tool, tool_messages, tool_use_messages, until, user_messages, user_words, words | `contains:"css refactor"` |

Machine clients can request parser-gated positives with MCP/CLI `query_completions(kind="example")` and real diagnostics/corrections with `query_completions(kind="error")`. The query capability resource carries corpus counts and the six shared semantics contracts.
<!-- END GENERATED: query-discovery -->
Expand Down
1 change: 1 addition & 0 deletions polylogue/archive/query/archive_execution.py
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,7 @@ def _summary_to_domain(summary: ArchiveSessionSummary) -> SessionSummary:
provider_project_ref=summary.provider_project_ref,
message_count=summary.message_count,
tags_m2m=summary.tags,
parent_id=SessionId(summary.parent_id) if summary.parent_id else None,
)


Expand Down
2 changes: 1 addition & 1 deletion polylogue/archive/query/discovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -1399,7 +1399,7 @@ def _example(
diagnostic=(
"unknown query field 'text'; recognized fields: action, assistant_messages, assistant_words, "
"authored_user_messages, authored_user_words, contains, cwd, duration_ms, has, id, lane, lineage, "
"messages, near, origin, paste_messages, path, project, repo, session, since, system_messages, tag, "
"messages, near, origin, paste_messages, path, project, repo, root, session, since, system_messages, tag, "
"thinking_messages, title, tool, tool_messages, tool_use_messages, until, user_messages, user_words, words"
),
field="text",
Expand Down
21 changes: 21 additions & 0 deletions polylogue/archive/query/expression.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,8 +156,10 @@
)
from polylogue.archive.query.spec import (
QUERY_ACTION_TYPES,
QuerySpecError,
SessionQuerySpec,
normalize_retrieval_lane,
optional_bool,
)
from polylogue.core.enums import Origin
from polylogue.core.errors import PolylogueError
Expand Down Expand Up @@ -3024,6 +3026,7 @@ class _SpecAccumulator:
max_messages: int | None = None
min_words: int | None = None
max_words: int | None = None
root: bool | None = None

def apply_token(self, tok: _LexToken) -> None:
"""Apply one token to the accumulator."""
Expand Down Expand Up @@ -3250,6 +3253,21 @@ def apply_token(self, tok: _LexToken) -> None:
field="lane",
) from exc

elif fname == "root":
if tok.negated:
raise ExpressionCompileError(
"use root:false instead of -root: to select non-root (subagent/branch) sessions",
field=fname,
)
if values:
try:
self.root = optional_bool("root", values[-1])
except QuerySpecError as exc:
raise ExpressionCompileError(
f"invalid root value {values[-1]!r}; expected root:true or root:false",
field="root",
) from exc

elif fname in COUNT_QUERY_FIELD_REGISTRY or fname in NUMERIC_QUERY_FIELD_REGISTRY:
# Already handled via _CountToken; field:value form without op is an error
raise ExpressionCompileError(
Expand Down Expand Up @@ -3295,6 +3313,7 @@ def to_spec(self) -> SessionQuerySpec:
max_messages=self.max_messages,
min_words=self.min_words,
max_words=self.max_words,
root=self.root,
)

def merge_from_spec(self, other: SessionQuerySpec) -> None:
Expand Down Expand Up @@ -3344,6 +3363,8 @@ def merge_from_spec(self, other: SessionQuerySpec) -> None:
self.min_words = other.min_words
if other.max_words is not None:
self.max_words = other.max_words
if other.root is not None:
self.root = other.root


# ---------------------------------------------------------------------------
Expand Down
3 changes: 3 additions & 0 deletions polylogue/archive/query/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -734,8 +734,11 @@ def sql_plan_value(self, plan: object) -> object:
),
QueryFieldDescriptor(
name="root",
spec_attr="root",
plan_attr="root",
spec_active=_not_none,
plan_active=_not_none,
spec_description=lambda value: "root" if value is True else "not root",
plan_description=lambda value: "root" if value is True else "not root",
requires_post_filter=True,
blocks_sql_count=True,
Expand Down
10 changes: 10 additions & 0 deletions polylogue/archive/query/metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,16 @@ class QueryPipelineStageInfo:
"negatable": "no",
"example": "title:refactor",
},
"root": {
"description": (
"Filter by top-level-vs-child session structure. root:true keeps only "
"top-level sessions; root:false keeps only subagent/branch children. "
"Unset (default) selects both."
),
"spec_field": "root",
"negatable": "no",
"example": "root:true",
},
"since": {
"description": "Filter sessions after date (ISO or relative: 7d, 2w)",
"spec_field": "since",
Expand Down
30 changes: 30 additions & 0 deletions polylogue/archive/query/spec.py
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,28 @@ def optional_int(value: object) -> int | None:
return int(str(value))


def optional_bool(field: str, value: object) -> bool | None:
"""Parse a tri-state boolean param: ``None`` means "unset", not ``False``.

Accepts native ``bool`` (from Click ``--flag/--no-flag`` pairs), and the
string forms ``true``/``false`` (case-insensitive, as used by the ``root:``
query-DSL field and JSON/MCP params). *field* names the caller's param for
the raised error.
"""
if value is None:
return None
if isinstance(value, bool):
return value
text = str(value).strip().lower()
if text == "":
return None
if text in {"true", "1", "yes"}:
return True
if text in {"false", "0", "no"}:
return False
raise QuerySpecError(field, str(value))


# Set of all recognized query-spec parameter names (drives strict-param mode).
_RECOGNIZED_PARAMS: frozenset[str] = frozenset(
{
Expand Down Expand Up @@ -241,6 +263,7 @@ def optional_int(value: object) -> int | None:
"message_type",
"offset",
"cursor",
"root",
}
)

Expand Down Expand Up @@ -359,6 +382,7 @@ def build_query_spec_from_params(
message_type=optional_message_type(params.get("message_type")),
offset=optional_int(params.get("offset")) or 0,
cursor=optional_text(params.get("cursor")),
root=optional_bool("root", params.get("root")),
)


Expand Down Expand Up @@ -425,6 +449,7 @@ def query_spec_to_plan(
offset=spec.offset,
cursor=spec.cursor,
boolean_predicate=spec.boolean_predicate,
root=spec.root,
vector_provider=vector_provider,
Comment on lines 449 to 453

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Push root down before plan pagination

When a spec is executed through the Python API/MCP plan path, this activates plan.root, but archive_execution._plan_filter_kwargs still omits it even though the storage readers now accept SQL-level root. The residual filter runs only after _archive_summaries has applied the requested offset and fetched at most limit * 4, so a page whose leading rows are children can return short or empty for root:true despite matching roots later in the archive (and conversely for root:false). Add root to the plan-to-storage kwargs so partitioning occurs before pagination.

AGENTS.md reference: AGENTS.md:L227-L229

Useful? React with 👍 / 👎.

)
if spec.latest:
Expand Down Expand Up @@ -485,6 +510,11 @@ class SessionQuerySpec:
offset: int = 0
cursor: str | None = None
boolean_predicate: QueryPredicate | None = None
#: Restrict to top-level sessions (``True``) or subagent/branch children
#: only (``False``); ``None`` (the default) selects both, unchanged from
#: historical behavior. See ``root:`` in the query DSL and ``--root/--no-root``
#: on the CLI (polylogue-oqib).
root: bool | None = None
#: Canonical query units to attach to each selected session as a
#: post-selection projection (the DSL ``with <units>`` clause). This is a
#: projection, not a filter/sort/limit, so it is deliberately absent from
Expand Down
2 changes: 2 additions & 0 deletions polylogue/cli/archive_query.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ class _ArchiveFilterKwargs(TypedDict):
since_ms: int | None
until_ms: int | None
since_session_id: str | None
root: bool | None
boolean_predicate: NotRequired[QueryPredicate]


Expand Down Expand Up @@ -304,6 +305,7 @@ def _execute_archive_query_stdout(env: AppEnv, request: RootModeRequest) -> None
"since_ms": since_ms,
"until_ms": until_ms,
"since_session_id": since_session_id,
"root": compiled_spec.root,
Comment on lines 305 to +308

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Apply root to analyze counts

For polylogue --root find ... then analyze --count (and the --no-root equivalent), the count_only branch below bypasses this shared filter_kwargs mapping and manually invokes count_search_sessions/count_sessions without root. The command therefore reports the unpartitioned total even though ordinary local list/search output applies the filter; pass root in both count calls or reuse this mapping.

AGENTS.md reference: AGENTS.md:L193-L197

Useful? React with 👍 / 👎.

}
if compiled_spec.boolean_predicate is not None:
filter_kwargs["boolean_predicate"] = compiled_spec.boolean_predicate
Expand Down
1 change: 1 addition & 0 deletions polylogue/cli/click_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,7 @@ def cli(
sort: str | None,
reverse: bool,
sample: int | None,
root: bool | None,
# Output
output: str | None,
output_format: str | None,
Expand Down
9 changes: 9 additions & 0 deletions polylogue/cli/click_option_groups.py
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,15 @@ def _validate_origin_tokens(
),
click.option("--reverse", is_flag=True, help="Reverse sort order"),
click.option("--sample", type=int, help="Random sample of N sessions"),
click.option(
"--root/--no-root",
"root",
default=None,
help=(
"Only top-level sessions (--root) or only subagent/branch children "
"(--no-root). Unset (default) selects both, unfiltered by structure."
Comment on lines +279 to +284

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Carry root through daemon query execution

When a daemon is reachable—the normal polylogued run setup—ordinary find pages take _try_emit_daemon_session_page, but _daemon_session_query_params never sends this new flag and _daemon_session_page_supported does not force a local fallback. Even root:true transported as DSL is discarded because polylogue/daemon/http.py::_archive_filter_kwargs_from_spec omits root; consequently --root, --no-root, and the DSL form return unfiltered rows on the daemon path. Thread the tri-state value through both daemon adapters or disable that fast path for root-filtered requests.

AGENTS.md reference: AGENTS.md:L193-L197

Useful? React with 👍 / 👎.

),
),
)

OUTPUT_OPTION_DECORATORS: tuple[Callable[[ClickCallable], ClickCallable], ...] = (
Expand Down
2 changes: 1 addition & 1 deletion polylogue/insights/correlation_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ def _print_otlp_evidence(env: AppEnv, session_id: str, output_format: str | None

def _enrich_with_github_api(result: SessionCorrelationResult) -> SessionCorrelationResult:
"""Cross-reference issue/PR refs against the GitHub API via gh CLI."""
from polylogue.insights.session_commit import GitHubRef
from polylogue.insights.session_commit import GitHubRef, SessionCorrelationResult

all_refs: list[tuple[GitHubRef, str]] = []
for ref in result.issue_refs:
Expand Down
Loading