loopctl-mcp-server 2.73.0 → 2.75.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.js +89 -12
- package/lib/client-context.js +34 -16
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -1497,9 +1497,16 @@ async function knowledgeFacets({
|
|
|
1497
1497
|
return toContent(result);
|
|
1498
1498
|
}
|
|
1499
1499
|
|
|
1500
|
-
async function knowledgeSearch({ q, project_id, story_id, category, tags, match, mode, limit, offset }) {
|
|
1500
|
+
async function knowledgeSearch({ q, project_id, story_id, category, tags, match, mode, format, limit, offset }) {
|
|
1501
1501
|
const params = new URLSearchParams();
|
|
1502
1502
|
if (q != null && q !== "") params.set("q", q);
|
|
1503
|
+
// `format` is the SHAPE of the response, not a different search (#678). The server
|
|
1504
|
+
// dispatches `stubs` to the same progressive_index/3 and `bodies` to the same
|
|
1505
|
+
// get_context/3 that knowledge_progressive_index and knowledge_context call, so those
|
|
1506
|
+
// tools remain and are not retired — they are now siblings on one path rather than
|
|
1507
|
+
// separate doors an agent has to choose between. That choice was unobservable and
|
|
1508
|
+
// therefore confounded any measurement of the ranking behind it.
|
|
1509
|
+
if (format) params.set("format", format);
|
|
1503
1510
|
if (project_id) params.set("project_id", project_id);
|
|
1504
1511
|
if (story_id) params.set("story_id", story_id);
|
|
1505
1512
|
if (category) params.set("category", category);
|
|
@@ -4616,6 +4623,21 @@ const TOOLS = [
|
|
|
4616
4623
|
enum: ["keyword", "semantic", "combined"],
|
|
4617
4624
|
description: "Optional: search mode (keyword, semantic, or combined).",
|
|
4618
4625
|
},
|
|
4626
|
+
format: {
|
|
4627
|
+
type: "string",
|
|
4628
|
+
enum: ["results", "stubs", "bodies"],
|
|
4629
|
+
description:
|
|
4630
|
+
"Optional: the SHAPE of the response, not a different search. 'results' " +
|
|
4631
|
+
"(default) is ranked results plus snippets and is the only shape that " +
|
|
4632
|
+
"supports cursor pagination. 'stubs' returns capped stubs with one hop of hub " +
|
|
4633
|
+
"enrichment — use it to survey a broad topic without pulling bodies into " +
|
|
4634
|
+
"context, then knowledge_progressive_drill into a chosen stub. 'bodies' " +
|
|
4635
|
+
"returns full article bodies plus linked references for one deep read. " +
|
|
4636
|
+
"'stubs' and 'bodies' REQUIRE a query; sending either without one is a 400, " +
|
|
4637
|
+
"as is an unknown value (it is never silently downgraded to 'results'). These " +
|
|
4638
|
+
"dispatch to exactly the same code knowledge_progressive_index and " +
|
|
4639
|
+
"knowledge_context call, which both remain available.",
|
|
4640
|
+
},
|
|
4619
4641
|
limit: {
|
|
4620
4642
|
type: "integer",
|
|
4621
4643
|
description: "Optional: maximum number of results to return.",
|
|
@@ -4643,7 +4665,12 @@ const TOOLS = [
|
|
|
4643
4665
|
"trustworthy answer plus its provenance rather than a ranked list to triage yourself; " +
|
|
4644
4666
|
"use knowledge_search when you want to browse/enumerate matches. Additive — existing " +
|
|
4645
4667
|
"knowledge tools are unchanged. If semantic ranking is unavailable it degrades to " +
|
|
4646
|
-
"keyword-only (meta.fallback/fallback_reason), same as knowledge_search
|
|
4668
|
+
"keyword-only (meta.fallback/fallback_reason), same as knowledge_search.\n\n" +
|
|
4669
|
+
"TODAY THE CURATED BRANCH IS UNREACHABLE ON THIS DEPLOYMENT: a source counts as " +
|
|
4670
|
+
"curated only if its article has curated_at set, and no article does — so every " +
|
|
4671
|
+
"call returns provenance 'retrieved' and a null curated_article_id. Read a " +
|
|
4672
|
+
"'retrieved' verdict as the normal case, not as evidence that a curated answer " +
|
|
4673
|
+
"was considered and rejected. This line comes out when something actually curates.",
|
|
4647
4674
|
inputSchema: {
|
|
4648
4675
|
type: "object",
|
|
4649
4676
|
properties: {
|
|
@@ -4839,8 +4866,10 @@ const TOOLS = [
|
|
|
4839
4866
|
properties: {
|
|
4840
4867
|
article_id: {
|
|
4841
4868
|
type: "string",
|
|
4842
|
-
|
|
4843
|
-
|
|
4869
|
+
description:
|
|
4870
|
+
"The UUID of the article. A unique ID PREFIX (>= 8 hex characters) also " +
|
|
4871
|
+
"resolves, so copy what you have rather than reconstructing 36 characters " +
|
|
4872
|
+
"from memory; an ambiguous prefix is a 404, never a guess.",
|
|
4844
4873
|
},
|
|
4845
4874
|
links: {
|
|
4846
4875
|
type: "string",
|
|
@@ -6469,7 +6498,40 @@ const TOOLS = [
|
|
|
6469
6498
|
"search_follow_through carries two further biases pointing OPPOSITE ways: the 20-row " +
|
|
6470
6499
|
"recording cap hides opens of results ranked beyond it (DOWN on large pages), while " +
|
|
6471
6500
|
"one open credits EVERY search in the window that surfaced that article, not just the " +
|
|
6472
|
-
"preceding one (UP when an agent refines and re-searches)
|
|
6501
|
+
"preceding one (UP when an agent refines and re-searches).\n\n" +
|
|
6502
|
+
"Exact attribution (unit: READS — not surfaced results, not calls): attributed_opens " +
|
|
6503
|
+
"/ cross_key_opens / direct_opens count READ rows by how their originating search was " +
|
|
6504
|
+
"established, resolved server-side at write time and never accepted from a caller. " +
|
|
6505
|
+
"Not comparable with followed_through, which counts SURFACED RESULTS later opened. " +
|
|
6506
|
+
"cross_key_opens is the population followed_through cannot see: it correlates on " +
|
|
6507
|
+
"api_key_id, and the injected recall hook searches under a different key from the " +
|
|
6508
|
+
"session that reads, so that channel scores a structural ZERO there — meaning " +
|
|
6509
|
+
"UNMEASURABLE, not unread. Cross-key attribution is circumstantial (two agents in one " +
|
|
6510
|
+
"tenant can reach one article independently), hence labelled rather than folded in. " +
|
|
6511
|
+
"direct_opens is the agent going straight to an article by link or cited id, which " +
|
|
6512
|
+
"used to look identical to 'surfaced and ignored' — close to its opposite.\n\n" +
|
|
6513
|
+
"Disposition (unit: SEARCH CALLS): searches_scored_with_follow_through, " +
|
|
6514
|
+
"searches_reformulated and searches_quiet PARTITION searches_scored — NOT searches. " +
|
|
6515
|
+
"Treating every not-opened search as a failure is wrong — an agent answered by the " +
|
|
6516
|
+
"result snippet correctly opens nothing, and that is a success. A reformulation (the " +
|
|
6517
|
+
"SAME SESSION issuing a later search call with a DIFFERENT QUERY in-window, nothing " +
|
|
6518
|
+
"opened) is the closest thing to an unambiguous failure, so it is split out; what " +
|
|
6519
|
+
"remains is `quiet` and is STILL a mixture of 'snippet sufficed' and 'rows ignored'. " +
|
|
6520
|
+
"This surface does not separate them — do not read quiet as either.\n\n" +
|
|
6521
|
+
"searches_scored is SMALLER than searches and the gap is NOT quiet traffic. A search " +
|
|
6522
|
+
"is scoreable only if it carries a session identity (stamped forward-looking, so a " +
|
|
6523
|
+
"pre-migration row reports searches_scored: 0) and comes from a channel that can " +
|
|
6524
|
+
"react to a result at all — the recall hook and the session-start auto-query emit one " +
|
|
6525
|
+
"distilled query per prompt and never see what came back, so they cannot reformulate " +
|
|
6526
|
+
"by construction. They stay in every other denominator here, precision included. " +
|
|
6527
|
+
"Read searches - searches_scored as n/a, never as zero.\n\n" +
|
|
6528
|
+
"COMPARE ROWS ONLY WITHIN A metric_version. Every row carries the version of the " +
|
|
6529
|
+
"definition set that produced it. Three changes have already altered what a figure here " +
|
|
6530
|
+
"MEANS — searched went from search calls to surfaced results, infrastructure traffic " +
|
|
6531
|
+
"began being excluded, and the disposition trio was rescoped — each forward-looking and " +
|
|
6532
|
+
"each previously leaving no mark on the row, so a series read across one of those " +
|
|
6533
|
+
"boundaries compares definitions rather than days. 0 means the row predates the stamp " +
|
|
6534
|
+
"and its definitions are unknown.",
|
|
6473
6535
|
inputSchema: {
|
|
6474
6536
|
type: "object",
|
|
6475
6537
|
properties: {
|
|
@@ -6491,8 +6553,15 @@ const TOOLS = [
|
|
|
6491
6553
|
{
|
|
6492
6554
|
name: "knowledge_analytics_top",
|
|
6493
6555
|
description:
|
|
6494
|
-
"Return the top
|
|
6495
|
-
"
|
|
6556
|
+
"Return the top READ knowledge articles for the tenant — articles whose body was " +
|
|
6557
|
+
"actually delivered (get/context/drill). Requires orchestrator role.\n\n" +
|
|
6558
|
+
"access_type DEFAULTS TO READS, not to every event. `search` and `index` rows are " +
|
|
6559
|
+
"IMPRESSIONS the ranker produced — one per surfaced result — and they outnumber reads " +
|
|
6560
|
+
"roughly 50:1, so the old unfiltered default ranked ranker output while claiming to " +
|
|
6561
|
+
"show what agents read. Pass access_type:'all' if you genuinely want impressions " +
|
|
6562
|
+
"counted, or a single type to select one. unique_keys counts distinct API KEYS, not " +
|
|
6563
|
+
"agents: v2 mints one ephemeral key per dispatch, so one agent dispatched N times is " +
|
|
6564
|
+
"N keys.",
|
|
6496
6565
|
inputSchema: {
|
|
6497
6566
|
type: "object",
|
|
6498
6567
|
properties: {
|
|
@@ -6525,8 +6594,12 @@ const TOOLS = [
|
|
|
6525
6594
|
{
|
|
6526
6595
|
name: "knowledge_article_stats",
|
|
6527
6596
|
description:
|
|
6528
|
-
"Return per-article usage statistics:
|
|
6529
|
-
"
|
|
6597
|
+
"Return per-article usage statistics: total_events (impressions included), " +
|
|
6598
|
+
"total_reads (bodies actually delivered — get/context/drill), unique_keys, a by-type " +
|
|
6599
|
+
"breakdown, and the 10 most recent events. Requires orchestrator role.\n\n" +
|
|
6600
|
+
"Read total_reads, not total_events, when you want usage: impressions outnumber reads " +
|
|
6601
|
+
"roughly 50:1. unique_keys counts distinct API KEYS rather than agents — v2 mints one " +
|
|
6602
|
+
"ephemeral key per dispatch.",
|
|
6530
6603
|
inputSchema: {
|
|
6531
6604
|
type: "object",
|
|
6532
6605
|
properties: {
|
|
@@ -6541,7 +6614,8 @@ const TOOLS = [
|
|
|
6541
6614
|
{
|
|
6542
6615
|
name: "knowledge_agent_usage",
|
|
6543
6616
|
description:
|
|
6544
|
-
"Return knowledge usage for an agent:
|
|
6617
|
+
"Return knowledge usage for an agent: total_reads (bodies actually delivered), " +
|
|
6618
|
+
"total_events (impressions included), unique articles, top read articles. " +
|
|
6545
6619
|
"Pass api_key_id (api_keys.id credential) OR agent_id (agents.id logical identity) — not both. " +
|
|
6546
6620
|
"Requires orchestrator role.",
|
|
6547
6621
|
inputSchema: {
|
|
@@ -6576,8 +6650,11 @@ const TOOLS = [
|
|
|
6576
6650
|
{
|
|
6577
6651
|
name: "knowledge_unused_articles",
|
|
6578
6652
|
description:
|
|
6579
|
-
"Return published articles that have not been
|
|
6580
|
-
"
|
|
6653
|
+
"Return published articles that have not been READ in the configured time window. " +
|
|
6654
|
+
"Use to identify dead-weight knowledge. Requires orchestrator role.\n\n" +
|
|
6655
|
+
"\"Not read\" means no get/context/drill. It deliberately does NOT mean \"no event\": " +
|
|
6656
|
+
"on that definition an article the ranker surfaces constantly and nobody ever opens " +
|
|
6657
|
+
"counted as USED, which made this blind to the largest class of dead weight there is.",
|
|
6581
6658
|
inputSchema: {
|
|
6582
6659
|
type: "object",
|
|
6583
6660
|
properties: {
|
package/lib/client-context.js
CHANGED
|
@@ -12,18 +12,34 @@
|
|
|
12
12
|
* remains the sole authority. The server stores these under a `client_` prefix so no later
|
|
13
13
|
* reader mistakes them for server-derived facts.
|
|
14
14
|
*
|
|
15
|
-
* WHAT IS ACTUALLY AVAILABLE
|
|
15
|
+
* WHAT IS ACTUALLY AVAILABLE — re-measured 2026-08-12 by reading /proc/<mcp-pid>/environ on
|
|
16
|
+
* a live session, which corrected two entries an earlier pass got wrong:
|
|
16
17
|
*
|
|
17
|
-
*
|
|
18
|
-
* CLAUDE_SESSION_ID=<uuid> -> session id, available
|
|
19
|
-
* CLAUDE_CODE_CHILD_SESSION=1 -> subagent vs main session, available
|
|
18
|
+
* CLAUDE_CODE_SESSION_ID=<uuid> -> session id, available
|
|
20
19
|
* CLAUDE_CODE_ENTRYPOINT=cli -> entrypoint, available
|
|
20
|
+
* CLAUDE_SESSION_ID -> ABSENT (the CODE_ spelling is the one that is set)
|
|
21
|
+
* CLAUDE_EFFORT -> ABSENT from THIS process. It is set for Bash-tool
|
|
22
|
+
* invocations, which is where the earlier claim that
|
|
23
|
+
* it was "available" came from; the MCP server does
|
|
24
|
+
* not get it, so `effort` is enriched offline.
|
|
25
|
+
* CLAUDE_CODE_CHILD_SESSION -> ABSENT, and it would not mean what it looks like:
|
|
26
|
+
* it is set to 1 for Bash-tool invocations of a MAIN
|
|
27
|
+
* session, so it marks "a child PROCESS", not "a
|
|
28
|
+
* dispatched agent".
|
|
21
29
|
* (no model variable) -> MODEL IS NOT AVAILABLE
|
|
22
30
|
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
31
|
+
* THE KIND REPORTED HERE IS THE SESSION'S, NOT THE CALLER'S. One MCP server process is
|
|
32
|
+
* spawned per session and serves the main session AND every agent it dispatches, and the
|
|
33
|
+
* environment above is read once and cached for the life of that process. So `kind` is a
|
|
34
|
+
* property of the session, and every search it labels comes back `main`. The three-way
|
|
35
|
+
* main/subagent/workflow split a measurement actually needs is recoverable only from the
|
|
36
|
+
* transcript, where `isSidechain` plus the file's path give it unambiguously.
|
|
37
|
+
*
|
|
38
|
+
* Two fields are therefore sent as JOIN KEYS rather than as answers: `session_id` is what
|
|
39
|
+
* lets `mix loopctl.enrich_search_events` find the transcript that records the model, the
|
|
40
|
+
* effort and the real kind. Note that a RESUMED session breaks even that — the new process
|
|
41
|
+
* reports a fresh session id while the transcript keeps appending under the original — so
|
|
42
|
+
* the enrichment carries a query-only fallback for exactly that case.
|
|
27
43
|
*/
|
|
28
44
|
|
|
29
45
|
import { execFileSync } from "node:child_process";
|
|
@@ -87,15 +103,17 @@ function clientContext({ version } = {}) {
|
|
|
87
103
|
version,
|
|
88
104
|
};
|
|
89
105
|
|
|
90
|
-
//
|
|
106
|
+
// The SESSION's kind — see the header. This process is shared by the main session and
|
|
107
|
+
// every agent it dispatches, so in practice this resolves to "main" for all of them and
|
|
108
|
+
// is NOT caller-level evidence. It is still worth sending: it is the only thing available
|
|
109
|
+
// before the offline enrichment runs, and the enrichment treats it as an assertion to be
|
|
110
|
+
// corrected rather than as a value to be preserved.
|
|
91
111
|
//
|
|
92
|
-
//
|
|
93
|
-
//
|
|
94
|
-
//
|
|
95
|
-
//
|
|
96
|
-
//
|
|
97
|
-
// what is observable and let the join supply the rest; do not guess a value that would
|
|
98
|
-
// then be analysed as fact.
|
|
112
|
+
// Do not try to widen it to three values from the environment. CLAUDE_CODE_WORKFLOWS is a
|
|
113
|
+
// feature flag present in main sessions too, and CLAUDE_CODE_CHILD_SESSION marks a child
|
|
114
|
+
// PROCESS rather than a dispatched agent. Guessing here would be worse than the null it
|
|
115
|
+
// replaces, because the number this column exists to support is a comparison BETWEEN
|
|
116
|
+
// kinds.
|
|
99
117
|
const child = env("CLAUDE_CODE_CHILD_SESSION");
|
|
100
118
|
if (child !== undefined) {
|
|
101
119
|
ctx.kind = child === "1" || child.toLowerCase() === "true" ? "child" : "main";
|